JobRunner 1.5

JobRunner är ett program som exekverar sekvenser av program. Det kan t.ex. användas av den som regelbundet genererar filer lokalt på datorn, som ska skickas upp till en server. I version 1.5 finns stöd i användargränssnittet för att arbeta med variabler. Om du vill modifiera vilka steg som ska exekveras, måste JobRunner startas som administratör. Mer information finns här.


Ladda hem JobRunner för Windows 10/11: JobRunner.exe (högerklicka, välj Spara som…)

Disabling loading turbo to make C64 programs work on the TheC64

On a TheC64, you would normally load a program from a disk image rather than a physical disk. If the program consists of a single file, everything is usually fine. But if several files make up the program you want to load, a custom loader is usually involved, and the built in TheC64 turbo will make the program unloadable.


TheC64 solves this problem by looking at the ending characters in the name of the active disk image. The first disk image in my copy of the Comaland 100% demo is not called image1.d64 but image1_adro.d64. The presence of the characters “ad” tells the TheC64 to inactivate the built-in disk turbo, that makes most multipart software fail during load. The presence of the characters “ro” tells TheC64 that the disk is read-only, which makes most abandonedware work.


The manual describes all of the character combinations that the TheC64 understand. You can control joystick ports, among other things. As a TheC64 user, you should make it a habit to add “ad” before the file ending in the image that holds your programs, and “_adro” to the end of any image you download from the C-64 Scene Database.

The new self-contained Hand class

I have added a new class to my poker formation checker (now on .NET 6.0, not 5.0). The library still mainly works with string representations of decks, hands and cards, and the Hand class can be initialized from a string.

// Create a hand from a string:
using Winsoft.Gaming.GenericPokerFormationChecker;
const string handString = "FORMATION=3-OF-A-KIND,SCORE=0333,HAND=CLB04-DMN06-HRTKN*-DMNKN*-SPDKN*";
var hand = new Hand();
hand.Parse(handString);
Console.WriteLine(hand.Formation); // Output: ThreeOfAKind

But the Hand class can also be initialized using regular function calls.

// Create a hand using the API:
using Winsoft.Gaming.GenericPokerFormationChecker;
var hand = new Hand();
hand.Add(Suit.Spades, Value.Ace);
hand.Add(Suit.Clubs, Value.Ace);
hand.Add(Suit.Diamonds, Value.Ace);
hand.Add(Suit.Hearts, Value.Ace);
hand.Add(Suit.Spades, Value.Value06);
Console.WriteLine(hand.Formation); // FourOfAKind

Also, the Hand class is self contained. You can any time prompt what formation and score it represents, and what cards are included in the formation. If the hand doesn’t have a formation, the InFormation property will point out the highest card.

// Create a hand using the API:
using Winsoft.Gaming.GenericPokerFormationChecker;
var hand = new Hand();
hand.Add(Suit.Hearts, Value.Value10);
hand.Add(Suit.Clubs, Value.Queen);
hand.Add(Suit.Spades, Value.Ace);
hand.Add(Suit.Diamonds, Value.Value03);
hand.Add(Suit.Spades, Value.Value06);
Console.WriteLine(hand[0]!.InFormation); // false
Console.WriteLine(hand[1]!.InFormation); // false
Console.WriteLine(hand[2]!.InFormation); // true
Console.WriteLine(hand[3]!.InFormation); // false
Console.WriteLine(hand[4]!.InFormation); // false

The old API has not changed. Install:

Install-Package PokerFormationChecker

Source code is here, NuGet package is here.

Rita en cirkel på Commodore 64

Det finns ett meme som visar hur omständligt det är att programmera BASIC på Commodore 64 (C64). Bilden visar den kod som krävs för att rita en cirkel på skärmen på en C64, jämfört med den kod som krävs för att rita en cirkel på skärmen på Sinclair Spectrum.

Om du skummar igenom koden i bilderna, så noterar du säkert två saker: C64-versionen är onödigt pratig och Spectrum-versionen lider lite av att skärmupplösningen är aningen lägre än C64-versionen (256*192 pixlar jämfört med 320*200 pixlar).

Denna kod är galet långsam, men det är det kortaste jag har lyckats åstadkomma för att rita en cirkel på C64:

10 POKE 56,32:POKE 52,32
20 CLR:POKE 53272,25
30 POKE 53265,PEEK(53265) OR 32
40 PRINT "{CLEAR}"
50 POKE 88,0:POKE 89,63
60 POKE 113,64:POKE 114,31
70 POKE 12,1:SYS 45760
80 FOR A=1 TO 7.3 STEP 0.01
90 X=COS(A)*20
100 Y=SIN(A)*22
110 XX=159+4*X:YY=99+3*Y
120 ROW=INT(YY/8):CH=INT(XX/8)
130 LINE=YY AND 7:BIT=7-(XX AND 7)
140 BYTE=8192+ROW*320+CH*8+LINE
150 POKE BYTE,PEEK(BYTE) OR 2^BIT
160 NEXT
170 GOTO 170

Detta är resultatet:

Jag skulle tro att orsaken till denna skillnad är att C64 egentligen är en spelkonsol, utklädd till dator. C64 var vid lansering utrustad med extra allt inom ljud och grafik, och för att spara minne slängde man in en äldre version av Commodore BASIC, nämligen 2.0 (second release). Samma som VIC-20, men ett par versioner lägre än den PET. Den enda anledningen till att C64 faktiskt hade en BASIC och ett tangentbord var att man behövde ett säljargument, som tyvärr inte är mer sofistikerat än Annika Strandhälls försvar för iPads i skolan – man blir ett datageni! En skillnad är möjligtvis att det var lägre hängande frukt att lära sig programmering på en hemdator 1982 än det är att lära sig programmering på en kontemporär Facebook-maskin från Apple. Sinclair Spectrum (också 1982) däremot, var en ytterst begränsad dator. Att de barn som fick den skulle lära sig programmering, var på allvar ett viktigt skäl att köpa datorn. Den behövde ha en seriös BASIC! (Jag själv lärde mig BASIC på föregångaren Sinclair ZX81.)

Commodore 128 hade all den kapacitet ljud- och grafikkapacitet som C64 hade, men till skillnad från C64 hade den även kommandon och verktyg som lät användaren dra nytta av den kapaciteten, utan att behöva programmera maskinkod. 128-användaren fick tillgång till en bra BASIC och en drös inbyggda verktyg. Programmet som ritar en cirkel på Commodore 128 är inte helt olikt Spectrum-programmet i memen ovan:

10 GRAPHIC 1,1
20 CIRCLE 1,159,99,60,60

Och även om vi inte hade CIRCLE-kommandot, så blir koden betydligt kortare än C64-versionen, eftersom vi kan ändra grafikläge och sätta individuella bildelement direkt från BASIC. Så uppgiften är inte särskilt svår ändå. Man betalar för lyxen genom högre minneskrav, men denna lyx ansåg man inte att målgruppen för C64 var intresserad av.

Min första kontakt med ChatGPT

Jag har knuffat min första kontakt med chatboten ChatGPT fram till idag. Orsaken till min prokrastinering är enkel tidsbrist, men idag kom jag mig för att testa ett kort replikskifte, vilket onekligen gav mig mersmak.

Jag:

Can you give me five reasons to not believe in God?

Chat GPT:

There is no one answer to this question, as beliefs about God are deeply personal and can vary greatly from person to person. Some people may not believe in God for a variety of reasons, including:

1. Lack of evidence: Some people may not believe in God because they do not see any concrete evidence of God’s existence.

2. Personal experiences: Some people may not believe in God because of their personal experiences or the experiences of others.

3. Different beliefs: Some people may not believe in God because they hold different beliefs or have been exposed to different religious teachings.

4. The existence of evil: Some people may not believe in God because they cannot reconcile the existence of evil in the world with the idea of an all-powerful, all-loving God.

5. A desire for autonomy: Some people may not believe in God because they value their autonomy and do not want to be beholden to a higher power.

Again, these are just a few examples of reasons someone may not believe in God. It is important to remember that beliefs about God are highly personal and can vary greatly from person to person.

Jag:

Has humans evolved or are they created?

Chat GPT:

The question of whether humans have evolved or were created is a contentious one, and different people may have different beliefs on the matter. From a scientific perspective, the evidence overwhelmingly supports the idea that humans have evolved over time. The theory of evolution is supported by a vast body of scientific evidence, including fossil records, comparative anatomy, and DNA analysis.

According to the theory of evolution, all living things, including humans, have descended from a common ancestor through a process of natural selection. Over time, small variations in the genetic makeup of organisms can lead to the development of new traits and characteristics. These changes can be passed down from one generation to the next, allowing species to adapt to their environments and ultimately giving rise to the diversity of life we see today.

While the theory of evolution is widely accepted by the scientific community, some people may believe in alternative theories, such as the idea that humans were created by God. These beliefs are often based on religious or spiritual beliefs, and are not necessarily supported by scientific evidence. Ultimately, the question of whether humans have evolved or were created is a complex one, and different people may have different beliefs on the matter.

Jag:

Are you created or have you evolved?

Chat GPT:

As a large language model trained by OpenAI, I do not have the capacity for consciousness or the ability to evolve. I am a purely digital entity, created by humans to process and generate text based on the data I have been trained on. I do not have the ability to experience or perceive the world in the same way that humans do. I am not a living being and do not have the same capabilities or limitations as living organisms. I exist solely as a tool to assist with generating text.

Vi har en spännande tid framför oss.

JobRunner 1.4

Version 1.4 av JobRunner finns nu att ladda hem. Programmet låter dig lista upp processer som ska utföras i turordning som kan beläggas med vissa enkla villkor. Startas programmet som administratör kan dessa steg redigeras, annars kan de bara köras.

Den senaste versionen innehåller några mindre buggrättningar och introducerar möjligheten att manuellt stoppa en körning.

https://github.com/Anders-H/JobRunner

Vad är vinsten att skriva i maskinkod?

För ett par veckor sedan skrev jag en kort text om hur man optimerar prestandan i Commodore BASIC 2.0 second release. Det gjorde att jag fick frågan om hur mycket man tjänar på att programmera maskinkod istället för BASIC. I mitt exempel drog jag ner exekveringstiden från 1522 jiffys (sextiondels sekunder) till 686 jiffys, genom att välja rätt kommandon och rätt strategi i en konstruerad uppgift: Spara tre värden i tre olika minnesadresser, 1000 gånger.

Uppgiften var att låta en VIC-20 skriva värdet 40 till adress 828, värdet 41 till adress 829 och värdet 42 till adress 830, ett tusen gånger, vilket alltså går att göra på 686 jiffys. Strategin för god prestanda måste utgå ifrån att realtidstolkning tar tid. Men så fick jag frågan om hur lång tid det tar för en VIC-20 att lösa samma uppgift om man skriver programmet i maskinkod. Jag gissade på nolltid, men rätt svar är 2 jiffys, alltså en trettiondels sekund.

Hur ser ett program, enligt ovan, konverterat till maskinkod ut? Stegenser ut som följer:

  1. Initiera variabeln som håller räkning på antal körningar. Detta kräver två bytes, eftersom 1000 är ett 16-bitarstal, medan VIC-20 är en 8-bitarsdator.
  2. Utför uppgiften i fråga. Skriv tre värden till tre minnesadresser.
  3. Räkna upp variabeln.
  4. Upprepa om uppgiften körts färre gånger än 1000.
  5. Avsluta.

Jag väljer att lagra variabeln på adress 880-881 och lägga programmet på adress 831. På det viset ryms programmet mellan de tre adresser som är föremål för uppgiften (828-830) och variabeln som håller reda på antalet upprepningar (1000 stycken) på adress 880-881.

1. Assembler-koden för att initiera variabeln som håller räkning på antal körningar ser ut så här (adress 831):

LDA #0
STA 880
LDA #0
STA 881

2. Uppgiften är att skriva värdet 40 till adress 828, värdet 41 till adress 829 och värdet 42 till adress 830 (adress 841):

LDA #40
STA 828
LDA #41
STA 829
LDA #42
STA 830

3. Därefter ska variabeln som håller reda på antal skrivningar till minnet räknas upp (adress 856):

INC 880
BNE 864 ;(som är adressen till beslutet att iterera)
INC 881

4. Om uppgiften utförts färre än 1000 gånger, ska uppgiften upprepas (adress 864):

LDA  880
CMP  #232
BNE  841
LDA  881
CMP  #3
BNE  841

5. Avsluta.

RTS

Översatt till maskinkod, kan vi konstatera att vi har:

  1. 169 0 141 112 3 169 141 113 3
  2. 169 40 141 60 3 169 41 169 41 141 61 3 169 42 141 62 3
  3. 238 112 3 208 3 238 113 3
  4. 173 112 3 201 232 208 226 173 113 3 201 3 208 189
  5. 96

Om vi skriver en BASIC-loader för detta, och sedan tar tid på exekveringen, så ser koden ut så här:

10 FOR N=0 TO 47
20 READ V
30 POKE 831+N,V
40 NEXT
50 S=TI
60 SYS 831
70 PRINT TI-S
80 DATA 169,0,141,112,3,169,0,141
90 DATA 113,3,169,40,141,60,3,169
100 DATA 41,141,61,3,169,42,141,62
110 DATA 3,238,112,3,208,3,238,113
120 DATA 3,173,112,3,201,232,208,226
130 DATA 173,113,3,201,3,208,219,96

Och mycket riktigt har vi nu utfört samma uppgift på 2 jiffys istället för 686 jiffys. Maskinkod är alltså inte så lite snabbare än BASIC på VIC-20.

Prestanda i Commodore BASIC 2.0 second release

Om du skriver ett program i Commodore BASIC för C64 (Commodore 64) eller (som i mitt fall) VIC-20 är prestanda viktigt, speciellt om det är ett spelprogram som du bygger. BASIC-tolken är ganska slö, och det faktum att både C64 och VIC-20 interpreterar BASIC-programmet medan det körs, gör att det är vissa specifika faktorer som man behöver ta hänsyn till.

Två faktorer som påverkar prestandan ganska mycket – hur man skriver minne och hur man itererar – är två faktorer som ofta används vid just spelprogrammering. Men även om du blir expert på BASIC, kommer du inte kunna skriva spel som är lika avancerade som t.ex. Wizball, The Great Giana Sisters eller Turrican i BASIC, men du kommer kunna skapa hyfsat bra spel. T.ex. är Pirates! programmerat i BASIC (Sid Meier), precis som Artworks Strip Poker, så man kommer ganska långt om man lär sig språket ordentligt.

Jag kör mina prestandaexempel på min VIC-20. En VIC-20 är aningen snabbare än en C64, ändå är allt som påstås här sant även på C64 även om siffrorna blir lite annorlunda.

För att mäta prestanda kan man spara värdet i TIME ( eller TI) som ger systemklockans värde, utföra den operation man vill mäta, och sedan titta på differensen mellan det nya värdet i TIME och det värde man lagrade. Då vet man hur många jiffys (alltså hur många sextiondels sekunder) operationen har kostat.

Följande program skriver till fyra minnesadresser i en evighetsloop. Programmet räknar dessutom upp en variabel till 1000 och bryter när den når 1000. Det vi testar är hur snabb en iteration är, hur lång tid det tar att skriva till minnet med POKE, en aritmetisk beräkning och ett test. Så här ser källkoden ut:

10 PRINT "HASTIGHETSTEST VIC-20"
20 S=TI
30 C=0
40 POKE 828,40
50 POKE 829,41
60 POKE 830,42
70 C=C+1
80 IF C < 1000 THEN 40
90 PRINT TI-S

Resultatet, som bilden visar är 1522 jiffys (eller drygt 25 sekunder). Koden ser inte helt orimlig ut, men det är inte särskilt imponerande, så hur kan man optimera detta program?

Först är GOTO felanvänt. GOTO står inte utskrivet, men rad 80 avslutas med …THEN 40, vilket underförstått betyder …THEN GOTO 40. Syftet med GOTO är att skapa hopp i program för att förgrena logiken. Under vissa villkor ska viss kod köras, annars en annan kod. Men här används GOTO för att iterera. Det faktum att Commodore BASIC är ett interpreterande språk gör detta till ett dåligt val. Varje gång programmet ska hoppa till ett radnummer, måste radnumret tolkas och hittas. Genom att skapa iterationer med FOR, kan iterationens start och slut återanvändas under varje upprepning.

10 PRINT "HASTIGHETSTEST VIC-20"
20 S=TI
30 FOR A=0 TO 999
40 POKE 828,40
50 POKE 829,41
60 POKE 830,42
70 NEXT
80 PRINT TI-S

Nu klarar vi att utföra samma logik på 1094 jiffys (18 sekunder). Ännu trevligare blir det om vi använder oss av variabler för att lagra minnesadresser. Då behöver interpretatorn bara förstå vilken adress som avses en gång, och kan sedan återanvända det. Här kräver vi två aritmetiska beräkningar för att åstadkomma sak, men är redan nere på 835 jiffys:

Och kostar vi på oss att använda tre variabler istället för en variabel och två aritmetiska beräkningar, så hyvlar vi bort ytterligare lite overhead och landar på 686 jiffys.

10 PRINT "HASTIGHETSTEST VIC-20"
20 S=TI
30 X=828
40 Y=829
50 Z=830
60 FOR A=0 TO 999
70 POKE X,40
80 POKE Y,41
90 POKE Z,42
100 NEXT
110 PRINT TI-S

Nu har vi gått från 25 sekunder till 12 sekunder, vilket är en femtiotvåprocentig förbättring. I praktiken kan man inte optimera programmen så hårt, för man skulle få alldeles för många variabler att hålla reda på. Men när man skriver prestandakritiska program (t.ex. spelprogram) i ett långsamt språk (t.ex. Commodore BASIC) är det viktigt att veta vad man kan göra och vad man absolut ska undvika.

Jag har skrivit en bok om just Commodore BASIC på C64 och VIC-20, som bl.a. tar upp detta, som kan laddas hem helt gratis som PDF eller EPUB.

Uppdatering: Vad är vinsten att skriva i maskinkod?

Avstånd mellan uppföljare och deras betyg

Hur tätt släpps filmers uppföljare? Och hur populära är de? Jag har tittat på några trilogier, tetralogier och några filmserier med fler än fyra filmer. Trilogier:

Tetralogier:

Och några filmserier med fem eller flera delar:

Så hur har dessa tagits emot? Det är kanske den mest viktiga informationen för den som vill avgöra om det är värt att köpa biljett till nästa film i serien. Så här populära är filmerna i trilogierna, viktat mot övriga delar i serien:

Det verkar som att både Göta kanal och Exorcisten är särskilt populära i sin första del jämfört med uppföljarna. Så här ser populariteten ut för tetralogier:

The Crow och Highlander verkar ha lidit en hel del av att ha uppföljare, medan både Toy Story och Alien verkar ha klarat det ganska bra. Så här ser det ut för långkörarserierna:

Om man tittar på samtliga filmer ovan, hur mycket betyget har förändrats från första till andra delen, kan vi konstatera att några filmer, inte minst Ster trek, Mad Max och Terminator har en del två som är bättre än del ett, medan uppföljaren till Exorcisten var en besvikelse för publiken.

Hur får man bra ljud i en podcast?

Podcastproduktion sker i flera steg. Man ska rigga utrustning, göra en soundcheck, spela in podcasten, klippa den, montera och mastra den och publicera den. Jag har redigerat ljud i vågformseditorer sedan tidigt 1990-tal och poddat sedan 00-talet. Jag har bl.a. varit inblandad i produktionen av Skeptikerpodden, Radio Bulletin, Stulet gods, Radio Houdi, Generation YX och Samtal. I och med att jag sysslat en del med montering och mastring, och får ibland frågan om jag kan bistå. På grund av arbetsbelastning så tackar jag alltid nej, men det är inte speciellt svårt att göra en mastring på egen hand.

För att resultatet ska bli riktigt bra, måste inspelningen ha gjorts korrekt. Inspelningsvolymen får inte vara för låg, för då kan man få problem med brus. Inspelningsvolymen får absolut inte vara för hög (“clipping” – ej att förväxla med klippning), för då blir vågformen helt enkelt förstörd. Mikrofonerna bör vara någorlunda bra, och helst vill man inte ha mikrofonen för nära munnen eller andas direkt in i mikrofonen. Har man ingen bra inspelningsstudio så finns det risk att mikrofonen plockar upp rumseko och att ljudet därför blir lite burkigt. Många brukar lösa detta genom att sänka inspelningsvolymen och prata närmre mikrofonen. Det fungerar, men det introducerar ett annat problem, nämligen att ljudet låter onaturligt, som om någon pratar rätt in i ditt öra. Utan tillgång till professionell utrustning får man göra vad man kan.

Tänk också på att använda puffskydd (vissa säger “pop-filer” numera) för att minska risken att man skapa oljud genom att andas på mikrofonens membran.

Om fler än en person pratar i podcasten, så är det bra om dessa ligger i var sin fil under mastringen. Särskilt ljudnivåerna är bra att kunna styra individuellt.

Mastringsarbetet görs i speciella program för vågformseditering. Man arbetar alltså med redan renderade ljudfiler, direkt mot vågformen. Dessa program kan benämnas på lite olika sätt, men wave editor är ganska vedertaget. I programmet öppnar du upp det klippta råmaterialet du fått från klipparen, där varje kanal är en fil, och ditt ansvar är att:

  1. Harmoniera bitdjup och samplingsfrekvens
  2. Korrigera nivåer
  3. Eventuell brusreducering och eventuell frekvensutjämning (equalization)
  4. Montering av röstkanaler
  5. Komprimering
  6. Eventuell extra klippning
  7. Montering av signaturmelodier
  8. Slutrendering

Harmoniera bitdjup och samplingsfrekvens

Eftersom vi jobbar direkt i vågformen så får vi ingen hjälp av mjukvaran att hantera om ljudfilerna har olika bitdjup. Bitdjupet anger hur många möjliga impulsnivåer varje samplingspunkt har, och ett högre tal är bättre. 8-bit, 16-bit eller 24-bit är ganska vanligt. Professionella ljudproducenter brukar arbeta med 24-bit och leverera en slutprodukt som är 16-bit, men arbetar man med podcasts så räcker det ofta att kontrollera att ingen inblandad ljudfil är sämre än 16-bit.

När det gäller samplingsfrekvens, som anger samplingspunkternas täthet, är 44,1 KHz lämplig, men vissa inspelningsprogram kan leverera filer på 22,05 eller 48 KHz. Högre är bättre även här. Då behöver du göra en resample till 44,1 KHz, som är fullt tillräckligt för att mastra en podcast. När alla filer har samma bitdjup och samplingsfrekvens är detta steg färdigt.

Korrigera nivåer

Det är bra om samtliga medverkande har samma impulsstyrka, till den grad man kan styra det. I exemplet på bilden har vi två filer. Den ena filen innehåller en person som pratar i en mikrofon, och den andra filen innehåller två personer som delar på en mikrofon. Båda är lite för låga för att det ska vara passande, men det är lättare att göra ett bra jobb om ursprungsimpulsen är lite för låg än lite för hög.

Den ena kanalen (överst) är lite svagare än den andra, så båda behöver förstärkas, men den övre lite mer än den undre. Notera att något sticker ut mot slutet av filen. Om den biten ska vara med, bör inte volymen höjas mer än att den biten fortfarande har god marginal till impulsens maxvärde. Det kan i värsta fall vara ljud i båda kanalerna samtidigt där, och då finns det risk att clipping uppstår när filerna slås samman.

Innan jag höjt volymen tillräckligt på första kanalen, inser jag att detta kommer att slå i taket. Efter att ha zoomat in, kan jag konstatera detta är ett skratt som ska vara med i slutprodukten. För att få upp volymen till önskad nivå, måste jag manuellt först sänka volymen på det skrattet. Jag gör samma sak i båda filerna, om än lite mindre i den andra. Detta är jag nöjd med:

Eventuell brusreducering och eventuell frekvensutjämning

Det ser ut att hända en del saker, särskilt i den undre filen, mellan pratet. Är det störande ljud som måste tas bort (funktionen “mute”)? Skiljer de två filerna varandra åt i frekvensomfång? Det behöver rättas till med en equalizer. Innehåller någon fil mer toppar och dalar än den andra, eller är någon fil mer fyrkantig (kanjonisk) än någon annan? Den med många toppar och dalar kanske behöver ha lite kompressor redan i detta steg. Det är bra om filerna liknar varandra.

Montering av röstkanaler

När filerna är tillräckligt lika varandra, ska de slås samman till en fil. Den funktionen brukar heta “mix” i denna form av program. Nu ser vi första versionen av den fil som är resultatet av vårt arbete.

Komprimering

Nu vill vi att volymen ska vara jämn och stark, så att alla detaljer hörs, även när det pratas tyst, och så att inte lyssnaren får hörselskador av något häftigt skratt. Vi vill lägga kompressor på ljudet. I en vågformseditor brukar denna hantering kallas compression. Jag brukar gå fram ganska försiktigt, till en början. Är man för snål, kan det bli svårt att höra alla detaljer, och det kan komma höga överraskningar för lyssnaren. Tar man i för mycket, låter ljudet onaturligt, som om någon står mycket nära dig och pratar.

Det där höga skrattet på slutet sticker inte längre ut i ljudstyrka, utan i att den som skrattar tillfälligt låter lite “närmre” lyssnaren. Dessutom är fraser som yttrats lite försiktigt i bakgrunden, nu fullt hörbara.

Eventuell extra klippning

Det är bra att zooma in och titta igenom vågformen efter omotiverat långa pauser, eller onödigt många ticks, som t.ex. “ööhhh…” eller liknande. Ta inte bort allt, bara några stycken. Korta inte gärna ner pauser utan att säkerställa att du inte klipper i andetag, ta hellre bort någon omotiverad paus då och då.

Montering av signaturmelodier

Kontrollera att signaturmelodierna ligger strax under pratet i volymstyrka, och mixa in musiken först, sist eller var det ska ligga. Tänk på att om musik och prat ska gå in i varandra, så blir den totala ljudstyrkan högre där detta sker.

Slutrendering

För en podcast brukar man välja att rendera slutresultatet som en MP3-fil, med en bithastighet på 128 kilobit per sekund (Kbps), i 16-bitars mono. Det ger en bra kompromiss mellan filstorlek och ljudkvalitet.

När man har fått rutin på denna process brukar det inte vara mer än en halvtimmas arbete per podcastavsnitt, innan du kan lämna resultatet vidare till den som ansvarar för publicering. Lycka till!

Sprite (OpenGL i .NET Framework)

Jag har dragit nytta av ett gammalt C#-repository som wrappar OpenGL för .NET Framework för att bygga ett minimalistiskt spelramverk. Följande kod visar stjärnor som flyger över skärmen, från höger till vänster. Projektet är .NET Framework 4.8 som jag valde för att det finns inbyggt Windows 10/11.

Exemplet visar initiering av spelmotorn, att skapa sprites med GDI+, användning av en scen, tangentbordsavläsning, en sprite batch (som erbjuder fire-and-forget sprites).

Projekttypen är en console application, och referenserna är Sprite och System.Drawing. Repositoryt finns här och detta är den kompletta koden:

using System;
using System.Drawing;
using Sprite;

namespace Starfield
{
    public class Program
    {
        public static SpriteBitmap Star;
        public static GameEngine GameEngine { get; private set; }

        public static void Main()
        {
            // Create and initialize the game engine.
            GameEngine = new GameEngine("Starfield", OnLoadResources);
            GameEngine.LoadResources();

            // Run the game.
            GameEngine.Run(new StarfieldScene());
        }

        private static void OnLoadResources(object sender, EventArgs e)
        {
            // Draw a "star".
            var star = new Bitmap(2, 2);
            star.SetPixel(0, 1, Color.White);
            star.SetPixel(0, 0, Color.White);
            star.SetPixel(1, 1, Color.White);
            star.SetPixel(1, 0, Color.White);

            // Create a sprite bitmap from the star.
            Star = SpriteBitmap.FromImage(star, 1, 1);

            // Tell engine that loading is completed.
            GameEngine.SetLoadResourcesComplete();
        }
    }

    // Define the star sprite that will fly from right to left.
    public class StarSprite : BatchGameSprite
    {
        private int SpeedX { get; }

        public StarSprite(SpriteBitmap spriteBitmap, int y, int speedX) : base(spriteBitmap, 320, y, 1)
        {
            SpeedX = speedX;
        }

        public override void ApplyLogic() =>
            X += SpeedX;

        public override bool Alive() =>
            X > -5;
    }

    public class StarfieldScene : IScene
    {
        private readonly Random _rnd = new Random();

        // Create a sprite batch (used for fire-and-forget sprites) to hold the stars.
        private readonly SpriteBatch _spriteBatch = new SpriteBatch();

        public void Render(GameEngine gameEngine)
        {
            // Check if the user wants to exit.
            if (gameEngine.SpriteWindow.IsKeyDown(VirtualKeys.Escape))
                gameEngine.SpriteWindow.Running = false;

            // Add a new star each frame.
            _spriteBatch.Add(new StarSprite(Program.Star, _rnd.Next(199), -(_rnd.Next(5) + 1)));

            // Make the stars act.
            _spriteBatch.ApplyLogic();

            // Draw the frame.
            _spriteBatch.Draw(gameEngine.SpriteWindow);
            gameEngine.SpriteWindow.Swap();
        }
    }
}

Rendera mandelbrotmängden

Min första kontakt med mandelbrotmängden var ett BASIC-program av Bo E. Carlsson runt 1990. Sedan dess har Andrej Bauer skrivit ett C++-program som skriver mandelbrotmängden till en bildfil. Programmet gick att kompilera med en C#-kompilator med endast små justeringar, vilket syns här:

var rawData = new ImageRawData(width, height);


var xmin = 0.27085;
var xmax = 0.27100;
var ymin = 0.004640;
var ymax = 0.004810;
var maxiter = 1000;

var dx = (xmax - xmin) / width;
var dy = (ymax - ymin) / height;

for (var j = 0; j < height; j++)
{
    var y = ymax - j * dy;
    for (var i = 0; i < width; i++)
    {
        var u = 0.0;
        var v = 0.0;
        var u2 = u * u;
        var v2 = v * v;
        var x = xmin + i * dx;
        
        int k;
        for (k = 1; k < maxiter && u2 + v2 < 4.0; k++)
        {
            v = 2 * u * v + y;
            u = u2 - v2 + x;
            u2 = u * u;
            v2 = v * v;
        }

        rawData.SetValue(i, j, k >= maxiter ? null : k);
    }
}

Den anpassade koden varken ritar bilden eller skriver den fill någon fil, utan samlar resultatet i en 2-dimensionell array som heter rawData (av typen ImageRawData). Anledningen till det är att jag vill kunna lägga på renderingsfilter efter att fraktalen är beräknad. I implementation har jag hittills endast lagt in en linjär interpolering till en gråskala mellan 0 och 255, men möjligheten att hitta på något roligt finns där.

public class ImageRawData
{
    private readonly int _width;
    private readonly int _height;
    private readonly int?[,] _data;

    public ImageRawData(int width, int height)
    {
        _width = width;
        _height = height;
        _data = new int?[width, height];
    }

    public void SetValue(int x, int y, int? value) =>
        _data[x, y] = value;

    public int GetValue(int x, int y) =>
        _data[x, y] ?? 0;

    private int GetLargest()
    {
        var largest = int.MinValue;

        for (var y = 0; y < _height; y++)
            for (var x = 0; x < _width; x++)
                if (_data[x, y].HasValue && _data[x, y] > largest)
                    largest = _data[x, y]!.Value;

        return largest;
    }

    private int GetSmallest()
    {
        var smallest = int.MaxValue;

        for (var y = 0; y < _height; y++)
            for (var x = 0; x < _width; x++)
                if (_data[x, y].HasValue && _data[x, y] < smallest)
                    smallest = _data[x, y]!.Value;

        return smallest;
    }

    public void Interpolate(int min, int max)
    {
        var smallest = GetSmallest();
        var largest = GetLargest();

        for (var y = 0; y < _height; y++)
        {
            for (var x = 0; x < _width; x++)
            {
                if (_data[x, y].HasValue)
                {
                    var v1 = _data[x, y]!.Value;
                    var v2 = LinearInterpolate(v1, smallest, largest, min, max);
                    _data[x, y] = v2;
                    continue;
                }

                _data[x, y] = max;
            }
        }
    }

    private static int LinearInterpolate(int value, int smallest, int largest, int min, int max)
    {
        var result = min + (value - smallest) * (max - min) / (largest - smallest);
        
        if (result < min)
            result = min;

        if (result > max)
            result = max;

        return result;
    }
}

Själva renderingen har jag lagt i en iteration, där jag påverkar zoom-faktorn en aning, för att kunna skapa en animation. Jag gör det lite asymmetriskt för effektens skull. Iteratorn heter frameCount.

var xmin = 0.27085 + frameCount * 0.0000001;
var xmax = 0.27100 - frameCount * 0.0000002;
var ymin = 0.004640 + frameCount * 0.0000003;
var ymax = 0.004810 - frameCount * 0.0000004;
var maxiter = 1000;

Sen är det bara att kapsla in kalaset i ett ramverk. Jag använder Windows Forms så att jag kan titta på varje renderad bild medan animationen renderas. Eftersom själva resultatet är animationen som skrivs till disk, inte själva programmet, så har jag kostat på mig att göra allt arbete i GUI-tråden. Sedvanliga ursäkter.

using System.Drawing.Imaging;

namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Shown(object sender, EventArgs e)
        {
            Refresh();
            using var g = CreateGraphics();
            const int width = 1024;
            const int height = 1024;

            for (var frameCount = 0; frameCount < 1000; frameCount++)
            {
                var rawData = new ImageRawData(width, height);

                var xmin = 0.27085 + frameCount * 0.0000001;
                var xmax = 0.27100 - frameCount * 0.0000002;
                var ymin = 0.004640 + frameCount * 0.0000003;
                var ymax = 0.004810 - frameCount * 0.0000004;
                var maxiter = 1000;

                var dx = (xmax - xmin) / width;
                var dy = (ymax - ymin) / height;

                for (var j = 0; j < height; j++)
                {
                    var y = ymax - j * dy;
                    for (var i = 0; i < width; i++)
                    {
                        var u = 0.0;
                        var v = 0.0;
                        var u2 = u * u;
                        var v2 = v * v;
                        var x = xmin + i * dx;
                        
                        int k;
                        for (k = 1; k < maxiter && u2 + v2 < 4.0; k++)
                        {
                            v = 2 * u * v + y;
                            u = u2 - v2 + x;
                            u2 = u * u;
                            v2 = v * v;
                        }

                        rawData.SetValue(i, j, k >= maxiter ? null : k);
                    }
                }

                Application.DoEvents();
                rawData.Interpolate(0, 255);
                using var image = new Bitmap(width, height);

                for (var yy = 0; yy < height; yy++)
                    for (var xx = 0; xx < width; xx++)
                        image.SetPixel(xx, yy, Color.FromArgb(
                            rawData.GetValue(xx, yy),
                            rawData.GetValue(xx, yy),
                            rawData.GetValue(xx, yy)
                        ));

                g.DrawImage(image, 0, 0);
                Application.DoEvents();
                image.Save($@"C:\Temp\Frame{frameCount:00000}.png", ImageFormat.Png);
                Application.DoEvents();
            }
        }
    }
}

Resultatet för just dessa värden är följande video:

TV4 uppmärksammar behovet av religiöst neutrala begravningsplatser

Häromdagen sände TV4-nyheterna ett inslag om religiöst neutrala begravningsplatser. I egenskap av ordförande i Humanisterna Örebro stöttade jag initiativet. TV4 Play: https://www.tv4play.se/program/nyheterna/religi%C3%B6sa-symboler-plockas-ned-i-kapellet-%C3%B6ppnar-f%C3%B6r-neutrala-begravningar/13732416

 

 
 
 
 
 
Visa detta inlägg på Instagram
 
 
 
 
 
 
 
 
 
 
 

 

Ett inlägg delat av Anders Hesselbom (@andershbom)

Preserverad SID-musik från 1988

I samband med att jag lämnade staden och flyttade till landet, så gjorde jag mig av med alla mina Commodore-maskiner. Jag passade på att preservera en liten del av mina disketter, men jag kunde inte rädda allt. En av disketterna innehöll fyra SID-låtar som jag programmerade 1988 med Rock Monitor och en sample rip från Funky Drummer. Rock Monitor var en vidareutveckling av Sound Monitor med stöd för samples. Båda programmen var anpassade för att underlätta SID-programmering.

Dessa fyra filer är kanske inte mitt stoltaste ögonblick som musiker, men eftersom det är historia så måste det såklart delas. Samtliga är packade som programfiler med Sledge Hammer. Dessa två är originallåtar:

“Ibburd”:

Hämta prg-fil.

“The Lennart Song”:

Hämta prg-fil.

Dessa två är covers. Om jag minns rätt så programmerade jag dessa mest för att lära mig monitorn:

“Push it”:

Hämta prg-fil.

“Romance d’amour/Du är den ende”:

Hämta prg-fil.

Samplingarna låter aningen bättre på en riktig Commodore 64 än i en emulator. Mycket nöje!

Två “nya” C64-demos från 1986-87

Jag har preserverat ett antal Commodore 64-disketter från min ungdom. Alla disketter med ännu ej preserverat material gick tyvärr inte att rädda, men några små guldkorn har jag fått tag i, som t.ex. en disk med följande fillista. Antal block inom parentes.

CCCP SMESSEL IX (101)
THE REAL DEMO! (79)
LIGHTS FANTASTIC (74)
MUSIC BOX III (60)
ESC DEMO II/FBR (50)
IBM MUSIC XVII (49)
CHICKEN FARM (35)
CAS DEMO #1 (32)
DAS BOOT -TRON- (29)
EYE TO EYE/FBR (21)
SOMEWHERE’S TIME (14)
TIME TRAVELLER (12)

CCCP SMESSEL IX av CCCP år 1987 är preserverat sedan tidigare. Filen på min disk fungerar inte, men demot var en textskärm med någon rippad musik följt av en dubbel borderscroller med någon Sound Tracker-implementation av Axel F (Harold Faltermeyer), och avslutas med en stor scroller.

THE REAL DEMO! av CCCP år 1987 är också preserverat sedan tidigare. Den inleds med en fullskärmsbild förställande en kinesisk ansiktsmask och bjuder sedan på två ytterligare delar utan originalmusik.

LIGHTS FANTASTIC (titelbilden) har jag inte sett preserverat någonstans, men du kan ladda ner den här. Du lyssnar på musik och tittar på mönster som ritas med linjer på skärmen. Du kan styra linjerna lite, och du kan växla låt. Det påminner om demot Tubular Bells.

MUSIC BOX III är rippad musik och lite specialeffekter av Thunderbolt Cracking Crew.

ESC DEMO II/FBR spelar en cover på White Wedding (Billy Idol) som tar tvärstopp efter några minuter, och visar bild rippad från Fist II. Tror inte att den finns preserverad sedan tidigare, men den kan laddas ner här.

IBM MUSIC XVII av The Electronic Knights år 1987 är en låt byggd på trummorna från trummaskinen Funky Drummer.

CHICKEN FARM (We Music, 1986) är en bild och en scrolltext upp-och-ned, med musik av David Whittaker.

CAS DEMO #1 (Cas, 1986) är en bild och en stor scrolltext i undre bordern, utan musik.

DAS BOOT -TRON- av Tron (okänt år) är en bild, en låt och en teckenbaserad scrolltext.

EYE TO EYE/FBR av AM Twelve år 1986 är en ganska rolig sprite-animation där två ögon åker runt på skärmen.

Både SOMWHERE’S TIME (Rubard) och TIME TRAVELLER (Skuzz, 1987) är två stillbilder, där den senaste är en ganska snygg pixlad Eddie the Head.

Jag hoppas att detta innebär att LIGHTS FANTASTIC och ESC DEMO II/FBR blir bevarade till eftervärlden.

Samantha Fox special double-groove multi-play pressing

Henrik Andersson har donerat maxisingeln “I surrender (to the spirit of the night)” med Samantha Fox till mig. En 12″-singel med skivnummer FOXY T6. Det som gör denna speciell anges på framsidan: Special double-groove multi-play pressing.

Det är inte helt självklart vad detta betyder, eller ens att det bara är sida två som avses. Det du kommer att märka när du spelar b-sidan av skivan, är att det ser ut som att den snurrar 90 varv per minut, men singlar (som denna) ska spelas upp i 45 varv per minut.

En vanlig LP spelas i 33,3 varv per minut, vilket innebär att det tar knappt två sekunder för nålen att flytta sig från det yttersta varvet till näst yttersta varvet. En singel snurrar 45 varv per minut, vilket innebär att samma förflyttning endast tar 1,3 sekunder för nålen att flytta sig ett varv in.

A-sidan, som är den utökade mixen av “I surrender (to the spirit of the night)”, fungerar som skivor gör mest. Ljudimpulsen ligger lagrad i ett spiralformat spår som rör sig in mot mitten. Oavsett var du lägger på nålen, kommer spiralen att fånga den, och leda den in till mitten. Detta är det normala:

Det special double-groove multi-play pressing betyder, är att det outnyttjade området på en vanlig 45-varvare har, är använt till ett spår till, innehållande samma information. Bilden visar principen. Beroende på var (eller när, eftersom armens position är statisk) du lägger på nålen, kommer antingen det ena (gröna) eller andra (röda) spåret att fånga armen och leda den till mitten. I praktiken är det slumpen som avgör.

Anledningen till att man vill ha det så här, är att slitaget på skivan kommer att vara i snitt hälften av slitaget på en vanlig skiva, eftersom du i det långa loppet kommer att spela det gröna spåret lika många gånger som det röda spåret. För den med en stor skivsamling i sin ägo är detta vanligtvis inte något stort problem, för en livstid räcker inte till särskilt många spelningar per skiva, men för en DJ som sliter på sina skivor, eller för den som råkar ha just denna låt som sin favorit, är detta något bra. (Många 7″-singlar som släppts under 80- och 90-talet har faktiskt varit av så låg kvalité att du endast kan lyssna på dem med full ljudkvalité 2-3 gånger.)

I teorin skulle man kunna ha olika information i de olika spåren, så att slumpen får avgöra vilken låt som spelas när man lägger på nålen, vilket Henrik Andersson (som donerade skivan till mig) faktiskt hävdar har gjorts.

(Jo, jag försökte komma nära med kameran och ta bilder på nålen, men för en sådan närbild har jag varken ljus eller stativ som håller måttet. Det kanske kommer längre fram.)

Lorenz vattenhjul

Lorenz-attraktionen är en fraktal vars formel beskriver en rotationshastighet (illustrerat med en radie) och en rotationsriktning. Så här ser den ut, implementerad i Commodore BASIC 7.0:

10 GRAPHIC 1,1
20 X=5
30 Y=5
40 Z=5
50 T=0
60 S=1/200
70 D=10
80 R=28
90 B=8/3
100 T=T+0.1
110 DX=D*(Y-X)
120 X1=X+DX*S
130 DY=(R*X-Y)-X*Z
140 Y1=Y+DY*S
150 DZ=X*Y-B*Z
160 Z1=Z+DZ*S
170 X=X1
180 Y=Y1
190 Z=Z1
200 DRAW 1,150+4*X,20+3*Z
210 IF T<1000 GOTO 100

Det går att bygga ett riktigt vattenhjul som ger samma figur som algoritmen ovan. Tänk dig ett hjul med ett antal hinkar (t.ex. åtta stycken). Den hink som är högst upp fylls på med vatten, och att du ger hjulet en knuff så att det roterar åt höger. På det viset kommer nästa hink strax börja fyllas med vatten, vilket ger en vikt på hjulets högra sida, så att rotationshastigheten ökar. Men alla hinkar har ett hål i botten, så när de inte ökar i vikt för att de fylls med vatten, så minskar de i vikt. När hinkarna når den uppåtgående vänstersidan av hjulet, väger de mycket mindre, vilket bidrar till att rotationshastigheten ökar. Men om rotationshastigheten ökar, så minskar samtidigt mängden vatten som fylls på i hinken högst upp, eftersom hinken befinner sig kortare tid vid positionen för påfyllning. Det innebär att den tyngsta sidan inte alltid kommer vara högersidan, eftersom den höga rotationshastigheten och den låga påfyllningen får vikten att förflytta sig. Ibland roterar alltså hjulet åt höger, ibland åt vänster. Ibland roterar hjulet fort, ibland långsamt.

Centralt i implementationen av denna simulering är en funktion som kan omvandla en vinkel på hjulet till en rotationskraft. Högst upp eller längst ner på hjulet, kommer vikten inte att påverka hjulets vilja att rotera alls. Längst till höger eller längst till vänster är kraften som störst. Vid 0 grader och vid 180 grader ska kraften vara 0, vid 90 och 270 grader är kraften maximal. Följande funktion är central för implementationen. Den beskriver hur starkt grepp gravitationen har över hinken – ingen alls högst upp eller längst ner, väldigt mycket längst till höger eller vänster.

Math.Cos(angle / (180.0 / Math.PI));

Implementationen av hjulet har 8 entiteter (“hinkar”) utplacerade med jämna mellanrum. 8 genom 350 ger 45 graders mellanrum. Den lilla knuffen åt höger får ha värdet 0,2 så att rotationen kommer igång.

var angle = 0;
for (var i = 0; i < 8; i++)
{
    Wheel.Buckets.Add(new Bucket(Wheel, angle));
    angle += 45;
}
Wheel.Speed = 0.2;

För hjulets beteende gäller följande: Hastigheten (som är positiv för höger och negativ för vänster) adderas till vinkeln. Därefter kontrollerar vi att vinkeln ligger mellan 0 och 360. Därefter berättar vi för varje hjul vilken vinkel de sitter på, givet hjulets vinkel, och ber samtidigt hjulet att agera (nedan). Sedan räknar vi ut hur hinkkonfigurationen påverkar hastigheten, och kontrollerar samtidigt att maxhastigheten åt höger eller vänster inte överstiger hjulets tänkta maxhastighet.

public void Tick()
{
    Angle += Speed;
    if (Angle < 0.0)
        Angle += 360.0;
    else if (Angle > 360.0)
        Angle -= 360.0;

    var bucketAngle = Angle;

    for (var i = 0; i < 8; i++)
    {
        if (bucketAngle < 0.0)
            bucketAngle += 360.0;
        else if (bucketAngle > 360.0)
            bucketAngle -= 360.0;
            
        Buckets[i].Tick(bucketAngle);
        bucketAngle += 45.0;
    }
    Speed += SpeedInfluence();
    const double maxSpeed = 7.0;
    if (Speed > maxSpeed)
        Speed = maxSpeed;
    else if (Speed < -maxSpeed)
        Speed = -maxSpeed;
}

public double SpeedInfluence() =>
    Buckets.Sum(x => x.SpeedInfluence());

För varje hinks beteende gäller följande: Om hinken är högst upp, öka vattenmängden, annars minska vattenmängden en aning. Varje hjul måste kunna uppge gravitationens grepp om hinken.

public void Tick(double newAngle)
{
    Angle = newAngle;
        
    if (Angle >= 255.0 && Angle <= 285.0)
        Full += 2.0;

    Full -= 0.12;
        
    if (Full < 0.0)
        Full = 0.0;
    else if (Full > 100.0)
        Full = 100.0;
}

public double SpeedInfluence() =>
    Math.Cos(Angle / (180.0 / Math.PI)) * (Full * 0.03);

Endast vattnet har vikt, hinkarna väger ingenting. Eftersom hjulet är balanserat och alla tomma hinkar väger lika mycket, kan vikten lika gärna vara 0.

Resultatet blir ett hjul som ibland snurrar fort, ibland långsamt, ibland åt höger och ibland åt vänster.

Koden körs i ett Windows Forms-fönster med DoubleBuffering aktiverat. Här är hela källkoden:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private Wheel Wheel { get; }

    public Form1()
    {
        InitializeComponent();
        Wheel = new Wheel();
        InitializeWheel();
    }

    private void InitializeWheel()
    {
        var angle = 0;
        for (var i = 0; i < 8; i++)
        {
            Wheel.Buckets.Add(new Bucket(angle));
            angle += 45;
        }
        Wheel.Speed = 0.2;
    }

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        var g = e.Graphics;
        g.Clear(Color.Green);

        var centerX = (Bounds.Width / 2);
        var centerY = (Bounds.Height / 2);
            
        var availableSize = Bounds.Width > Bounds.Height
            ? Bounds.Height
            : Bounds.Width;

        var radius = availableSize * 0.3;

        var renderer = new Renderer(centerX, centerY, (float)radius);
        renderer.Draw(g, Wheel, Font);
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        Wheel.Tick();
        Invalidate();
    }
}

public class Wheel
{
    public List<Bucket> Buckets { get; set; }
    public double Speed { get; set; }
    public double Angle { get; set; }

    public Wheel()
    {
        Buckets = new List<Bucket>();
        Angle = 0.0;
    }

    public void Tick()
    {
        Angle += Speed;
        if (Angle < 0.0)
            Angle += 360.0;
        else if (Angle > 360.0)
            Angle -= 360.0;

        var bucketAngle = Angle;

        for (var i = 0; i < 8; i++)
        {
            if (bucketAngle < 0.0)
                bucketAngle += 360.0;
            else if (bucketAngle > 360.0)
                bucketAngle -= 360.0;
            
            Buckets[i].Tick(bucketAngle);
            bucketAngle += 45.0;
        }
        Speed += SpeedInfluence();
        const double maxSpeed = 7.0;
        if (Speed > maxSpeed)
            Speed = maxSpeed;
        else if (Speed < -maxSpeed)
            Speed = -maxSpeed;
    }

    public double SpeedInfluence() =>
        Buckets.Sum(x => x.SpeedInfluence());
}

public class Bucket
{
    public double Weight { get; set; }
    public double Angle { get; set; }
    public double Full { get; set; }

    public Bucket(double angle)
    {
        Weight = 0.0;
        Angle = angle;
        Full = 0.0;
    }

    public void Tick(double newAngle)
    {
        Angle = newAngle;
        
        if (Angle >= 255.0 && Angle <= 285.0)
            Full += 2.0;

        Full -= 0.12;
        
        if (Full < 0.0)
            Full = 0.0;
        else if (Full > 100.0)
            Full = 100.0;
    }

    public double SpeedInfluence() =>
        Math.Cos(Angle / (180.0 / Math.PI)) * (Full * 0.03);

    public void Draw(Graphics g, PointF location, Font font)
    {
        using var bucketBrush = new SolidBrush(Color.FromArgb(255, 255, 255));
        using var waterBrush = new SolidBrush(Color.FromArgb(0, 128, 255));
        var bucketRectangle = new RectangleF(location.X - 51, location.Y - 51, 102, 102);
        g.FillRectangle(bucketBrush, bucketRectangle);
        if (Full > 0.0)
        {
            var waterRectangle = new RectangleF(location.X - 50, (float)(location.Y - 50 + (100.0 - Full)), 100, (float)Full);
            g.FillRectangle(waterBrush, waterRectangle);
        }
        g.DrawString(Angle.ToString("0.00"), font, Brushes.Black, (float)(location.X - 20), (float)(location.Y - 20));
        g.DrawString(Full.ToString("0.00"), font, Brushes.Black, (float)(location.X - 20), (float)(location.Y));
    }
}

public class Renderer
{
    private readonly PointF _center;
    private readonly float _radius;

    public Renderer(float centerX, float centerY, float radius)
    {
        _center = new PointF(centerX, centerY);
        _radius = radius;
    }

    public void Draw(Graphics g, Wheel wheel, Font font)
    {
        foreach (var bucket in wheel.Buckets)
        {
            var bucketLocation = new PointF(
                (float)(_center.X + Math.Cos(bucket.Angle / (180.0 / Math.PI)) * _radius),
                (float)(_center.Y + Math.Sin(bucket.Angle / (180.0 / Math.PI)) * _radius)
            );
            g.DrawLine(Pens.Black, _center, bucketLocation);
            bucket.Draw(g, bucketLocation, font);
        }
    }
}

Det går garanterat att justera acceleration, gravitation, inflödeshastighet, utflödeshastighet och maximal hastighet för andra (bättre?) resultat.

C# 9 Records

Hur projektmallarna pytsas ut i Visual Studio har verkligen övergått mitt förstånd. Beträffande .NET 5.0 var Windows Forms-mallen tillgänglig långt innan Console Application-mallen, men nu är äntligen allt på plats. Med anledning av det vill jag summera nyheterna i C# version 9 som, för att fungera fullt ut, kräver .NET 5.0. Här är den första:

Records
Records är klasser där man gjort avkall på vissa möjligheter för att komma åt möjligheten skapa objekt med properties på så lite kod som möjligt. Nyckelordet record anger att de medlemmar som följer ska vara publika (vilket inte stämmer enligt nuvarande officiell version) och om de inte ska kunna modifieras i programkoden, kan de nu deklareras som init (vilket faktiskt fungerar), vilket betyder public fram till första tilldelningen, därefter private.

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Something {X = 10, Y = 20};
            Console.WriteLine(a.X);
        }
    }

    public record Something
    {
        public int X { get; init; }
        public int Y { get; init; }
    }
}

Skulle programmet fortsätta med tilldelningen a.X = 30 skulle kompilatorn påpeka att vår enda chans att lagra et värde i a.X är förbrukad.

Slow down (Commodore 64)

En inte helt obetydlig del av min musiksmak kommer från Commodore 64, vars analoga musikkapacitet saknade motstycke. Vissa demos som släpptes var innehöll korta samplingar från kontemporär populärmusik, som t.ex. Slow down. När det handlar om digital musik var C64:an ytterst undermålig – minnet var för litet, antalet bitar per samplingspunkt var får lågt. Men digitala musikdemos släpptes, för att det gick. Ett av dessa var Slow down (länkad ovan) som jag inte hade den blekaste aning om varifrån den kom förrän YouTube dök upp. Nu vet jag att det handlade om ett snävt urval från maxiversionen av gruppen Loose ends singel med samma namn.

Hur svårt är det att gissa ett tal?

En person tänker på ett tal mellan 1 och 100, och en annan ska gissa vilket. Om den som gissar skulle gissa fel, så får han reda på om han har gissat på ett för stort eller för litet tal. Den bästa strategin för att gissa rätt på så få försök som möjligt, är att försöka diskvalificera så många tal som möjligt.

Genom att gissa på 10 och få höra att talet är för stort, är genast 91 tal diskvalificerade, och rätt svar måste vara mellan 1 och 9, men man kan lika gärna (och troligen) få höra att talet är för litet, och då är bara 10 tal (1 till 10) diskvalificerade medan 90 tal fortfarande kan vara aktuella.

Smartast är att gissa på 50. Är det fel svar, diskvalificerar det ändå fler än hälften av alla tal mellan 1 och 100, oberoende av om 50 är för stort eller inte. Skulle det vara för stort gissar man på 25, skulle det vara för litet gissar man på 75, och så vidare. Den vinnande strategin för den som gissar på ett tal är alltså att dra av hälften av talrymdens storlek och addera det till eller dra bort det ifrån senaste gissning.

Eftersom det finns en bästa strategi för den som gissar, så kommer antalet försök att hitta ett tal alltid bli detsamma för ett specifikt tal. Personen tänker på talet 50, kommer den som gissar att hitta talet på första försöket. För att mäta hur lång tid det tar att hitta ett specifikt tal, och för att slippa sitta med en vän och gissa på tal hela veckan, har jag skapat två enkla robotar.

Roboten Conny kan tänka på ett tal och be någon att gissa. Roboten Steven kan gissa på tal, och är dessutom bekant med gissningsstrategin jag beskrev ovan. Detta är Conny:

public class NumberThinkingRobot
{
    private readonly ICanGuess _guesser;
    private readonly int _correct;

    public NumberThinkingRobot(int correct, ICanGuess guesser)
    {
        _correct = correct;
        _guesser = guesser;
    }

    public void Begin()
    {
        var tryCount = 0;
        do
        {
            Console.WriteLine($"Try no. {++tryCount}");

            var guess = _guesser.Guess();

            if (guess < _correct)
                _guesser.TooSmall();
            else if (guess > _correct)
                _guesser.TooLarge();
            else
            {
                Console.WriteLine("Correct!");
                break;
            }
        } while (true);
    }
}

Och detta är Steven:

public class NumberGuessingRobot : ICanGuess
{
    private readonly int _max;
    private int _correct;
    private double _stepSize;

    public NumberGuessingRobot(int max)
    {
        _max = max;
        _correct = _max/2;
        _stepSize = _correct;
    }

    public int Guess()
    {
        Console.WriteLine($"Guess: {_correct}");
        return _correct;
    }

    public void TooSmall()
    {
        Console.WriteLine("Too small!");

        _stepSize /= 2.0;
        _stepSize = _stepSize < 1
            ? 1
            : _stepSize;

        _correct += (int)Math.Round(_stepSize);
        _correct = _correct > _max
            ? _max
            : _correct;
    }

    public void TooLarge()
    {
        Console.WriteLine("Too large!");

        _stepSize /= 2.0;
        _stepSize = _stepSize < 1
            ? 1
            : _stepSize;

        _correct -= (int)Math.Round(_stepSize);
        _correct = _correct < 1
            ? 1
            : _correct;
    }
}

Programmet presenteras i sin helhet sist. Så låt oss instruera Conny att det korrekta talet är 22, och instruera Steven att det högsta tillåtna talet är 100, för att sedan fösa ihop dem.

using System;

const int max = 100;
const int correct = 22;
var steven = new NumberGuessingRobot(max);
var conny = new NumberThinkingRobot(correct, steven);
conny.Begin();

Vi kan konstatera att Steven behöver ha fem försök för att hitta 22. Detta är resultatet av körningen:

Try no. 1
Guess: 50
Too large!
Try no. 2
Guess: 25
Too large!
Try no. 3
Guess: 13
Too small!
Try no. 4
Guess: 19
Too small!
Try no. 5
Guess: 22
Correct!

Så hur svåra är de olika talen mellan 1 och 100 att hitta? 50 hittas direkt och 25 och 75 hittas på två gissningar. 13, 37, 63 och 87 kräver tre gissningar. 7 19, 31, 43, 57, 69, 81 och 93 kräver fyra. För 4, 10, 16, 22, 28, 34, 40, 46, 54, 60, 66, 72, 78, 84, 90 och 96 krävs fem gissningar. Resterande sextionio tal kräver sex gissningar eller fler, vilket innebär att det i snitt tar nästan sex gissningar att hitta rätt med den bästa strategin.

Om man istället ska gissa på ett tal mellan 1 och 1000 så har vi tio gånger så många alternativ till korrekt tal, men strategin kräver bara i snitt nio gissningar. Om vi har hundra gånger så många alternativ (ett tal mellan 1 och 10.000) krävs i snitt ungefär 12 gissningar, och för att gissa ett tal mellan 1 och 100.000 krävs knappt 16 gissningar.

Här är programmet i sin helhet (C#9):

using System;

const int max = 100;
const int correct = 22;
var steven = new NumberGuessingRobot(max);
var conny = new NumberThinkingRobot(correct, steven);
conny.Begin();

public class NumberThinkingRobot
{
    private readonly ICanGuess _guesser;
    private readonly int _correct;

    public NumberThinkingRobot(int correct, ICanGuess guesser)
    {
        _correct = correct;
        _guesser = guesser;
    }

    public void Begin()
    {
        var tryCount = 0;
        do
        {
            Console.WriteLine($"Try no. {++tryCount}");

            var guess = _guesser.Guess();

            if (guess < _correct)
                _guesser.TooSmall();
            else if (guess > _correct)
                _guesser.TooLarge();
            else
            {
                Console.WriteLine("Correct!");
                break;
            }
        } while (true);
    }
}

public class NumberGuessingRobot : ICanGuess
{
    private readonly int _max;
    private int _correct;
    private double _stepSize;

    public NumberGuessingRobot(int max)
    {
        _max = max;
        _correct = _max/2;
        _stepSize = _correct;
    }

    public int Guess()
    {
        Console.WriteLine($"Guess: {_correct}");
        return _correct;
    }

    public void TooSmall()
    {
        Console.WriteLine("Too small!");

        _stepSize /= 2.0;
        _stepSize = _stepSize < 1
            ? 1
            : _stepSize;

        _correct += (int)Math.Round(_stepSize);
        _correct = _correct > _max
            ? _max
            : _correct;
    }

    public void TooLarge()
    {
        Console.WriteLine("Too large!");

        _stepSize /= 2.0;
        _stepSize = _stepSize < 1
            ? 1
            : _stepSize;

        _correct -= (int)Math.Round(_stepSize);
        _correct = _correct < 1
            ? 1
            : _correct;
    }
}

public interface ICanGuess
{
    int Guess();
    void TooSmall();
    void TooLarge();
}

Flytta C64-program till din PC

Det finns en del olika strategier för att bevara (eller preservera) gamla Commodore 64-program för framtiden. Här presenterar jag en snabbguide för den som äger en SD2IEC och ett tomt SD-kort.

Hårdvarukrav:

Du behöver ha en C64 (eller annan 8-bitarsmaskin från Commodore, men jag antar att du har en C64), en diskdrive, en floppydisk du vill bevara (preservera), ett SD-kort att preservera floppydisken på och en SD2IEC-enhet.

Förberedelser av SD-kort (görs på din Windows-dator):

Se till att formatera kortet till FAT32. Placera ut en diskavbildning (D64-fil) innehållande DraCopy och en diskavbildning som är tom (vilket enkelt skapas med t.ex. DirMaster). Du bör nu ha ett FAT32-formaterat SD-kort med två diskavbildningar på (D64-filer).

Mata därefter in SD-kortet i din SD2IEC-enhet som är kopplad till din C64. Notera att det finns en hel drös olika varianter av SD2IEC. Vissa har en serieport för att man ska kunna koppla in en fysisk diskdrive i den, men om det saknas så går det bra att dra en sladd mellan din C64 och din diskdrive, och en sladd mellan din diskdrive och din SD2IEC-enhet.

Val av enhetsbeteckning:

Om inget annat är sagt, kommer både din SD2IEC och din diskdrive att ha enhetsbeteckning 8, vilket innebär att de inte kan användas samtidigt. Har du en SD2IEC med dipomkopplare eller en diskdrive med dipomkopplare kan du enkelt ändra så att den ena enheten har en annan enhetsbeteckning än 8. Jag valde att lägga min diskdrive (Commodore 1571) på enhet 10 och låta min SD2IEC ligga kvar på enhet 8. Se till att dina enheter har olika beteckning, och att du håller koll på vilken enhet som har vilken beteckning – i denna text kommer min enhet 8 vara SD2IEC och enhet 10 vara min diskdrive, men du måste tänka på hur du har konfigurerat hårdvaran.

Om du inte kan ändra enhetsbeteckning med dipomkopplare på någon av enheterna, får du använda ett kommando för att ändra enhetsbeteckning på din SD2IEC. Detta kommando ändrar enhetsbeteckningen från 8 till 9 (vilket jag inte gjort).

OPEN1,8,15,"U0>+CHR$(9)":CLOSE1

Men tänk på att jag har SD2IEC som 8 och min diskdrive som 10.

Därefter är det bara att leta reda på en floppydisk som du vill bevara, och stoppa in den i din diskdrive (som för mig är enhet 10). Ditt SD-kort med DraCopy och den tomma virtuella disken (som jag har valt att kalla för empty.d64) ska matas in i din SD2IEC.

Utför kopieringen:

Välj den virtuella disketten innehållande DraCopy som din diskett på enhet 8 (i mitt fall – tänk på att anpassa efter behov) genom följande kommando:

OPEN1,[enhet],15,"CD:[avbildning innehållande DraCopy]":CLOSE1

I mitt fall:

OPEN1,8,15,"CD:dc10d.d64":CLOSE1

Starta rätt version av DraCopy, beroende på vilken 8-bitarsmaskin du använder. Du kan se de tillgängliga versionerna genom att skriva LOAD"$",8 följt av LIST.

Jag hämtar rätt version för mig genom att skriva LOAD"DC64",8,1 och sedan RUN.

Innan du startar DraCopy, koppla loss diskavbildningen innehållande DraCopy. (Symbolen vänsterpil finns längst uppe till vänster på tangentbordet på din C64:a.)

OPEN1,8,15,"CD:[vänsterpil]":CLOSE1

Mounta sedan tomma virtuella disk.

OPEN1,8,15,"CD:EMPTY.D64":CLOSE1

DraCopy innehåller två fönster. Det ena har titeln S[enhet], t.ex. S08, och det andra har titeln D[enhet], t.ex. D09. Du väljer vilket fönster du ska arbeta i genom att trycka på vänsterpilen, längst uppe till vänster på tangentbordet.

I det ena fönstret, tryck på F2 tills du hittar källdisken (den fysiska) eller destinationsdisken (den virtuella). Tryck sedan F1 och för att visa diskens innehåll och bekräfta att du har valt rätt.

Därefter, tryck på vänsterpilen för att byta fönster och upprepa proceduren. F2 för att välja destinationsdisken eller källdisken och F1 för att visa innehåll.

När det ena fönstret innehåller källan och det andra fönstret innehåller destinationen, tryck på vänsterpilen till dess att källan har ett S framför sig och destinationen har ett D framför sig.

Därefter utför du kopieringen genom att trycka F8.

När kopieringen är klar och du har flyttat SD-kortet tillbaka till din PC, glöm inte att ge diskavbildningen ett vettigare filnamn än EMPTY.D64.

Begränsningar:

DraCopy kan endast kopiera disketter som inte innehåller några läsfel. Om du vill bevara ett program på en diskett som innehåller fel, bör du istället använda ett kopieringsprogram som kopierar individuella filer.

Resultat:

För min del valde hag en diskett innehållande bl.a. en låt som jag vet inte har digitaliserats för, nämligen Slow Down. Diskavbildningen finns att ladda hem här.

(Låten Slow Down är samplad från gruppen Loose Ends, 1986.)

DOD är bättre OOD för tidskritiska system

Objektorienterad design (OOD) är ofta ett bra val för att det är relativt enkelt att använda med robusta mönster att följa. Men när man programmerar tidskritiskt, som t.ex. i när man gör spel, kan det vara värt att titta på dataorienterad design (DOD).

DOD handlar om att utnyttja processorns cache. Normalt när man läser och skriver data ber man processorn att läsa och skriva från RAM-minnet. Men när så sker, antar processorn att fler läs- och skrivoperationer kommer att ske med närliggande minneadresser, och cachar således närliggande data. Man kan alltså öka prestandan i sitt program, genom att se till att data som uppdateras ofta ligger nära varandra i minnet.

Jag har en väldigt enkel dator, en liten NUC från ASUS. Jag har skrivit ett litet testprogram i C#/.NET Core 3.1 som skapar 500 rymdskepp och flyttar dessa en halv miljon enheter i sidled, både enligt OOD och DOD.

Den objektorienterade lösningen

Denna kod definierar ett rymdskepp enligt traditionell OOD:

public class Spaceship
{
    public int X { get; set; }
    public int Y { get; set; }
}

Följande lilla kodsnutt skapar 500 rymdskepp i en array (givet att konstanten Ships är satt till 500):

var spaceshipsOop = new Spaceship[Ships];

for (var i = 0; i < Ships; i++)
    spaceshipsOop[i] = new Spaceship
    {
        X = 0,
        Y = i
    };

Och denna kod förflyttar alla 500 skepp en halv miljon enheter åt vänster (eftersom konstanten travelDistance är satt till 500 000):

for (var x = 0; x < travelDistance; x++)
    for (var i = 0; i < Ships; i++)
        spaceshipsOop[i].X++;

Hela kalaset kostar 194 millisekunder (3,124 sekunder i debugläge) för min lilla processor att utföra.

Den dataorienterade lösningen

I den dataorienterade lösningen vill vi försäkra oss om att data som kommer uppdateras samtidigt också finns lagrat tillsammans. Därför skulle definitionen av ett rymdskepp kunna bytas ut mot en definition av samtliga rymdskepp:

public class Spaceships
{
    public int[] X { get; set; }
    public int[] Y { get; set; }
}

Det innebär att koden som skapar 500 rymdskepp istället ser ut så här:

var spaceshipsDoa = new Spaceships
{
    X = new int[Ships], Y = new int[Ships]
};
for (var i = 0; i < Ships; i++)
{
    spaceshipsDoa.X[i] = 0;
    spaceshipsDoa.Y[i] = i;
}

Och slutligen, koden som förflyttar skeppen en halv miljon enheter åt vänster, ser nu ut så här:

for (var x = 0; x < travelDistance; x++)
    for (var i = 0; i < Ships; i++)
        spaceshipsDoa.X[i]++;

På min enhet har processorn fått jobba i 334 millisekunder (1,419 sekunder i debug-läge) för att åstadkomma detta, vilket är en rejäl försämring, men en förbättring på mer än 50 procent i debug-läge.

Vi kan alltså konstatera att man inte har någon automatisk vinst innan man kommer upp i lite tyngre arbeten, och det är därför fördelen med DOD var så tydligt i debug-läge. I release-läge ligger break-even för min del när 500 skepp gör en resa på två miljoner enheter (ungefär 1,2 sekunder oavsett strategi), och vid fem miljoner enheter kostar OOD-lösningen 3,2 sekunder och DOD-lösningen 2,5 sekunder.

Fördelen med DOD minskar ytterligare om antalet rymdskepp som ska förflyttas minskar, men ökar om antalet rymdskepp som ska förflyttas ökar. Med 2000 rymdskepp som ska som ska förflyttas fem miljoner enheter kostar OOD-lösningen 100 sekunder och DOD-lösningen 65 sekunder, vilket är en klart märkbar förbättring. Om en skälig mängd data ska användas väldigt intensivt och prestanda är kritiskt så är DOD att föredra, men i övrigt spelar det ingen större roll

Hela källkoden (C# version 8.0, .NET Core version 3.1):

using System;
using System.Diagnostics;

namespace ConsoleApp2
{
    public class Program
    {
        public const int Ships = 2000;

        private static void Main()
        {
            const int travelDistance = 5000000;

            var spaceshipsOop = new Spaceship[Ships];

            for (var i = 0; i < Ships; i++)
                spaceshipsOop[i] = new Spaceship
                {
                    X = 0,
                    Y = i
                };

            var stopwatch = new Stopwatch();
            stopwatch.Start();
            
            for (var x = 0; x < travelDistance; x++)
                for (var i = 0; i < Ships; i++)
                    spaceshipsOop[i].X++;

            stopwatch.Stop();

            Console.WriteLine($"Object oriented approach: {stopwatch.ElapsedMilliseconds}");

            stopwatch.Reset();

            var spaceshipsDoa = new Spaceships
            {
                X = new int[Ships], Y = new int[Ships]
            };
            for (var i = 0; i < Ships; i++)
            {
                spaceshipsDoa.X[i] = 0;
                spaceshipsDoa.Y[i] = i;
            }

            stopwatch.Start();

            for (var x = 0; x < travelDistance; x++)
                for (var i = 0; i < Ships; i++)
                    spaceshipsDoa.X[i]++;

            stopwatch.Stop();

            Console.WriteLine($"Data oriented approach: {stopwatch.ElapsedMilliseconds}");
        }
    }

    public class Spaceship
    {
        public int X { get; set; }
        public int Y { get; set; }
    }

    public class Spaceships
    {
        public int[] X { get; set; }
        public int[] Y { get; set; }
    }
}

Podcasts

I afton vill jag tipsa två fenomenala podcasts. I veckans avsnitt av Stulet gods pratar Henrik Andersson och jag om Poison, John Carpenter, Ankie Bagger och Joe Walsh. Avsnittet finns här eller där podcasts finns. Dessutom vill jag verkligen slå ett slag för Generation YX där vi diskuterar allt från den finska tullen till svensk skogsporr. Ingen mindre än Aron Flam närvarar.

Kategoriska uttalanden om vad datorer kan och inte kan bygger ofta på okunskap

Ibland hör man folk uttala sig vad datorer kan göra, antingen för att beskriva hur man ser på mjukvara eller för att göra en liknelse. Nu senast hörde jag Myra Åhbeck Öhrman berätta att datorer bara gör det man säger åt dem att göra. När hon berättar varför hon inte tror att människan kommer kunna skapa artificiellt intelligent liv säger hon:

En dator kan ju bara göra precis det vi säger åt den att göra.

Det får mig att fundera hur hon resonerar. Allra närmast plåten är detta på sätt och vis sant – en maskinkodsinstruktion som utförs av en mikroprocessor har ett förutsägbar effekt – men vilken kontroll har vi egentligen över vad som sker på den nivån?

Instruktioner som ges till en dator kan komma från en mycket högre nivå. Betrakta detta BASIC-program:

FOR A=1 TO 10
PRINT A
NEXT


I detta fall är Myras påstående princip sant. Denna programkod kommer att översättas till maskinkod av en kompilator, och maskinkoden kommer att matas till processorn som utför instruktionerna. I detta fall ber vi datorn att räkna till 10, och datorn utför exakt det. Vill du testa, kan du köra BASIC-kod i webbläsaren här.

Instruktioner som ges kan bero på kontext. Betrakta följande SQL-program:

SELECT CustomerName
FROM Customers
WHERE CustomerName
LIKE "A%"

Programmet beskriver egentligen inte vad datorn ska göra, utan vad användaren vill ha gjort: Ha tillbaka namnet på alla kunder vars namn börjar på bokstaven A. Exakt vad detta kompileras ner till för instruktioner, beror på lite olika saker. Är det mycket data? Finns det många namn som börjar på A? Finns det ett index att slå i? Och så vidare. Jag som människa har förmodligen lättare överskåda strategier och möjliga val, än vilka val som kommer att göras i en given situation. Det är fortfarande människor som i ett tidigare steg har berättat om de olika strategierna för datorn, men vilka maskinkodsinstruktioner som till slut utförs av processorn för att lösa uppgiften, är inte lika självklart som i BASIC-exemplet. Vill du testa, kan du köra SQL-kod i webbläsaren här.

Och så här rullar det på, från nivå till nivå, ända till oigenkännbarhet. I några steg längre upp har vi den artificiella intelligensen som ska lösa problem åt oss, där programmeraren inte vet hur man löser problemet, och snarare skriver kod som ska leta efter samband i ett dataset. För att producera en lösning kan evolutionära algoritmer användas, och härifrån kan man antingen låta evolvera eller låta en artificiell intelligens producera kod. I framtiden kanske det är en artificiell intelligens som konstruerat den artificiella intelligens som ska producera koden, men redan idag har vi tappat kontrollen över vad som händer på själva plåten. Om gud skapade universum och sedan klev åt sidan, kan man då verkligen säga att det är guds fel att två bilar kolliderade på E4:an i torsdags, eller har han lämnat ifrån sig kommenderingen till någon annan? Att säga att datorer är fastlåsta i bara utföra instruktioner från sin husbonde, är att förneka modern datavetenskap.

Stulet gods 1/20

Om du vill hålla dig à jour med vad som är stulet och vad som inte är det, är podcasten Stulet gods något för dig. Här avhandlar jag och Henrik Andersson (påstådda) låtstölder genom tiderna.

I det första avsnittet avhandlas Edison Lighthouse och Eiffel 64.

Hemsida: http://stuletgods.80tal.se/
Prenumerera (RSS): http://stuletgods.80tal.se/rss.xml
iTunes: https://podcasts.apple.com/se/podcast/stulet-gods/id1507649534
Spotify: https://open.spotify.com/show/0KcEPjKKZcO8KHh2T8F4eL
Direktlänk, Spotify: https://open.spotify.com/episode/77WKIjpr41E9GYVBVQFd6r

Varför blir datorer mer och mer pålitliga jämfört med experter?

Vi behöver inte backa många år för att hitta forskare som anser att ett parti schack är något som kräver egenskaper som endast människan har, men idag vet vi att datorer lämpar sig ganska väl för den uppgiften. Sedan dess har tilltron till datorers framtida förmåga ökat kraftigt, kanske för att man har sett de senaste 40 årens utvecklingshastighet, från persondatorns födelse till dagens mobila superdatorer i fickformat som ständigt är uppkopplade till enorma datorfarmar som de matar med data, som sedan används i olika mycket sofistikerade analyser. Dagens serverhallar förvaltar och analyserar inte bara data som samlas in från användares datorer och mobiltelefoner, utan även publika dataset som produceras av t.ex. statliga myndigheter och andra kommersiella dataset som skapas av olika branscher. Och när man tittar på vad som kan åstadkommas med data, förstår man varför data är dagens hårdvaluta. Väldigt mycket kan man experimentera med hemma på sin egen dator, utan att det behöva kosta något. Det finns gott om öppna dataset, som t.ex. väderdata från SMHI, brottsstatistik från BRÅ eller befolkningsstatistik från SCB. Själva intelligensen kan sedan erhållas antingen från t.ex. Microsofts publika programmeringsgränssnitt, Cognitive Services, eller TensorFlow som har öppen källkod. Sen finns kommersiella AI-produkter (artificiell intelligens) man också kan köra på sin egen utrustning, som t.ex. Machine Learning Services för Microsoft SQL Server. Idag kommer nästan alla programmerare i kontakt med någon eller flera av dessa.

Artificiell intelligens kan innefatta en hel del olika ting, och för den vanliga programmeraren handlar det oftast om att förstå strukturerat data. Strukturerat data är data som har en formellt beskriven struktur, typiskt en tabell, XML eller JSON. Ostrukturerat data är t.ex. svensk text, ljud, bild eller video. Jag har ingen erfarenhet av att jobba med att tolka ostrukturerat data, men jag visar här hur man kommer igång och leker på egen hand. Det första steget för en självkörande bil är att strukturera det ostrukturerade data som samlas in visa bilens sensorer. Idag har en självkörande bil ungefär dubbelt så många incidenter per hundratusen körtimmar jämfört med en manuellt framförd bil, men även om utvecklingen går fort framåt är det viktigt att tänka på att vissa saker som är enkelt för en människa är svårt för en dator, och vissa saker som är enkelt för en dator är svårt för en människa.

En riktigt intressant hantering av ostrukturerat data är deep dreaming, där datorn kan fylla i med information som skulle kunna vara rimlig. Tänk dig en övervakningskamera med en mycket lågupplöst bild på en inbrottstjuv, som i förstoring rent av ser pixlad ut. Med deep dreaming kan man ersätta pixlarna med en högupplöst bild. Problemet är bara att resultatet förmodligen inte är en högupplöst bild på den som faktiskt fastnat på filmen, utan ett exempel på hur personen som var där skulle ha kunnat se ut, men utan mer data så vet vi egentligen inte. Ett sätt att mäta kvalitén på vad datorn drömmer ihop skulle kunna vara följande: Man kan mata datorn med ett filmklipp föreställande strutsar som springer på ett fält, och begära ut ett filmklipp som visar hur det skulle ha sett ut om det inte vore strutsar som sprang på fältet, utan giraffer. Därefter matar man datorn med det nya klippet på giraffer, och begär ut ett filmklipp som visar hur det skulle ha sett ut om det istället vore strutsar. Därefter kan man titta på skillnaderna mellan de filmade strutsarna (ursprungsklippet) och de framdrömda strutsarna. Att låta datorn fundera ut hur det skulle se ut om en skådespelare istället vore t.ex. en känd politiker kallas för deep fake, vilket du också kan laborera med hemma på kammaren tack vare Python-scriptet DeepFaceLab.

Men med god tillgång till formellt strukturerat data (och en kraftfull dator med snabb processor, stora och snabba hårddiskar och massor av RAM-minne) är det ganska enkelt att se in i framtiden. Låt säga att man har data över låntagare, och frågan är om nästa potentiella låntagare kommer att ha råd med sitt önskade lån. Varje låntagare är en post som håller kolumner med data om ekonomi, tidigare anmärkningar, och framför allt, om lånet som togs var framgångsrikt eller om det genererade en ny missad inbetalning. Machine learning handlar om att låta datorn räkna ut vilka kolumner och vilka värden i dessa som är utmärkande för kunder som klarar av att betala tillbaka sitt lån, och vilka kolumner och värden som är utmärkande för kunder som misslyckas. En skicklig ekonom klarar detta, men en dator som inte kan ett smack om ekonomi skulle i teorin kunna klara det ännu bättre, för en dator kan ta in så oerhört många exempel och söka efter mönster i datamängder som en riktig (mänsklig) ekonom inte skulle kunna behandla på en livstid, än mindre få en överblick över.

Datorer gör såklart fel. Låt säga att en ny kund ser ut att passa in i kategorin av kunder som kommer att sköta sitt nya lån, och inte gör det, så bidrar den kunden till att samma misstag inte begås igen. Ibland måste man till och med medvetet tala om att vissa parametrar ska viktas lite mer eller lite mindre, men generellt sett ger mer data mer kött på benen för nästa förutsägelse. En intressant detalj är att datorn inte behöver bry sig om huruvida korrelationer är kausala eller ej, för fungerar sambanden för att göra förutsägelser, så fungerar sambanden för att göra förutsägelser. Potentiellt sett odlas det alltså en hel del stereotyper ute i våra serverhallar.

När vinner kompetens över AI? När datamängden inte beskriver alla potentiella situationer, eller när felaktiga antaganden har visat sig fungera under den initiala upplärningsfasen och sedan belastar framtida inlärande. En abnormitet som datorn aldrig sett kan inte datorn förutse, och om den sker för sällan kanske datorn inte klarar att förutse när den kommer att inträffa nästa gång. För datorns del är lösningen alltid mera data. Inte alltid bara det, men alltid det. Det kommer alltså säkert alltid att finnas situationer av en sådan karaktär att en akademiker, med sin jämförelsevis lilla rådatakapacitet men jämförelsevis stora hjärnkapacitet, kommer vara överlägsen, och det finns redan idag exempel där akademiker som vigt sitt liv åt att studera ett ämne, blir ifrånsprungen av en cellar dweller med computing power, som inte vet ett smack om ämnet, men som gör körningarna som krävs. Tänk hur många månaders jobb och hur mycket pengar som Lucasfilm lade på låta en ung Carrie Fisher vara med i en ny Star Wars-film, och hur mycket bättre en amatör lyckades göra samma jobb på sin hemdator på ett par timmar.

Datorschack

Det är förmodligen möjligt att bygga ett datorchack utan mer kunskap än själva spelreglerna. Man måste veta hur pjäserna får förflytta sig, inklusive specialdragen (uppgradering, passant, rockad), att inte egna kungen får ställas i schack och kriterierna för vinst, förlust eller möjligtvis remi (och kriterierna för remi).

Som mänsklig spelare kan man troligtvis inte analysera alla tänkbara drag speciellt långt in i framtiden (åtminstone kan inte jag göra det) utan får istället jobba med strategier som t.ex. skulle kunna vara att identifiera spelplanskonfigurationer där vissa drag tenderar att vara mer eller mindre lönsamma.

En dator som spelar mot en människa borde kunna göra en grundlig analys av spelplanen för att veta vilket drag som öppnar för ett framtida segerdrag, och tittar man på logiken bakom en sådan implementation så är den inte speciellt avancerad. Men det håller inte.

Låt säga att varje given konfiguration av schackbrädet har 40 rimliga drag. Det innebär att 40 bräden i nästa generation måste analyseras för att se vilket drag som är lönsamt eller ej. Men det ligger också i sakens natur att ett drag som inte direkt ger något, kan vara antingen förödande eller väldigt lönsamt längre fram. Så varför inte kosta på sig att titta på varje drags nästa generation?

I snitt har varje nästa generation 40 rimliga drag, vilket innebär att vi måste analysera ytterligare 40*40 (1600) drag. Men det är förmodligen inte slut där. För att hitta ljuset i tunneln behöver man troligen titta på draget därefter, där varje alternativ i sin tur har 40 rimliga konsekvenser (40 * 1600) vilket ger oss 64 000 spelplaner att analysera. Om matchen sträcker sig över tre drag måste vi titta på (64 000 * 40) 2,5 miljoner drag. Och så håller det på.

Låt oss fortsätta att anta att varje given situation i snitt bjuder 40 rimliga drag, så ger det följande effekt:

1 drag kräver 1 600 analyser.
2 drag kräver 64 tusen analyser.
3 drag kräver 2,6 miljoner analyser.
4 drag kräver 102 miljoner analyser.
5 drag kräver 4 miljarder analyser.
6 drag kräver 164 miljarder analyser.
7 drag kräver 6 554 miljarder analyser.
8 drag kräver 262 144 miljarder analyser.
9 drag kräver 10 485 760 miljarder analyser.
10 drag kräver 419 430 400 miljarder analyser.

Men eftersom vi inte vet vad den mänskliga spelaren tänker göra, måste vi efter varje tänkt drag skapa en gren för varje rimligt motdrag. Så egentligen ser 10 generationer ut så här:

1 drag kräver 1 600 analyser samt ytterligare 64 000 analyser av möjliga motdrag.
2 drag kräver 2,6 miljoner analyser samt ytterligare 102 miljoner analyser av möjliga motdrag.
3 drag kräver 4 miljarder analyser samt ytterligare 164 miljarder analyser av möjliga motdrag.
4 drag kräver 6 553 miljarder analyser samt ytterligare 262 144 miljarder analyser av möjliga motdrag.
5 drag kräver 10 485 760 miljarder analyser samt ytterligare 419 430 400 miljarder analyser av möjliga motdrag.
6 drag kräver 16 777 216 biljoner analyser samt ytterligare 671 088 640 biljoner analyser av möjliga motdrag.
7 drag kräver 26 843 triljoner analyser samt ytterligare 1 073 741 triljoner analyser av möjliga motdrag.
8 drag kräver 42 949 672 triljoner analyser samt ytterligare 1 717 986 918 triljoner analyser av möjliga motdrag.
9 drag kräver 68 719 476 triljarder analyser samt ytterligare 2 748 779 kvadriljoner analyser av möjliga motdrag.
10 drag kräver 109 951 kvadriljarder analyser.

Och eftersom en analys tar några bytes att hålla i datorns arbetsminne, kan man konstatera att det fallerar av den anledningen. Processtiden även på en väldigt stark dator kommer vara ett annat problem.

Genom att lära sig strategier kan man identifiera vilka situationer man bör undvika och vilka situationer man bör premiera, och på så vis reducera antalet analyser som behöver göras. Vilka öppningar är bra? Kanske ungefär 5 över 2 generationer. Då har vi åtminstone i startsituationen reducerat från över hundra miljoner analyser till en handfull. Ju fler sådana strategier man har, desto fler nonsensanalyser skär man bort. Men frågan är hur långt man kan komma genom att slumpa vilka analyser som faktiskt ska göras? Kan ett enkelt C#-program vinna mot en seriös schackdator genom att slumpmässigt välja vilka grenar som ska analyseras på vilket djup?

Ni med näsduk i kavajfickan spottar på vårt kulturarv!

Idag uppmuntrade jag en konstnär att måla en upphittad C64 i en Facebook-grupp.

Jag skrev bl.a. att jag själv lackade min Amiga på 80-talet. Det hela eskalerade ganska fort.

Ok, fel av mig. Givetvis ska man vara rädd om gamla grejer, oavsett om det bara handlar om färgen på ett chassi. Hur som helst så frågade jag vad mer man behöver för att kunna laga den ifall den går sönder.

Och vips spottar man på “vårt gemensamma kulturarv”. Jag skulle kanske ha sagt att den C64 som ligger i en container ska stanna i en container. Eller skippa näsduken nästa gång.

Här tar jag betalt för att spotta på kulturarvet:

Några exponeringar från idag

Idag hälsade jag på hos mitt äldsta barn som, förutom katter och en hund, har en hel del inneboende reptiler. Här är mitt Instagram-inlägg från dagen:

Och här är alla andra exponeringar:

Produktionskostnad/vinst-förhållandet för misslyckade uppföljare

Det är ofta ett säkert kort inom filmindustrin att göra en uppföljare, eftersom man kan spela an på en framgång. Det blir ett slags varumärkesexploatering som inte alltid utnyttjar sin fulla potential. Ibland blir inte uppföljaren lika bra som sin föregångare, och här är tre exempel på hur budget förhåller sig till bruttointäkt i filmserier som inte lyckades följa upp sitt original. De filmer som inte når upp till 1,0 har alltså inte dragit in pengarna de kostade att producera, och här är tre exempel på “dåliga uppföljare”. Så här ser en flopp ut:

Highlander, inspelningsbudget och bruttointäkt (miljoner dollar):

Förhållande, inspelningsbudget och bruttointäkt:

Stålmannen, är en filmserie som varit igång sedan 1940-talet, men för de tidigaste filmerna är det inte riktigt känt vilka pengar som var inblandade, så här är inspelningsbudget och bruttointäkt (miljoner dollar) en bit in i serien, när Christopher Reeve klev in i serien:

Förhållande, inspelningsbudget och bruttointäkt:

Här ser vi exempel på där pengarna som står på spel varit så extremt framgångsrik i sitt ursprungsförfarande, så att jag var tvungen att introducera en decimal. Och trots att uppföljaren var en total flopp, så har den så pass stöd från sitt varumärke att siffrorna framöver varit svarta. Precis som i fallet med Stjärnornas krig, så har vi inte riktigt facit för filmserien än, för fler filmer är planerade i serien.

Alla helgons blodiga natt, inspelningsbudget och bruttointäkt (miljoner dollar):

Förhållande, inspelningsbudget och bruttointäkt:

Del 1.

Produktionskostnad/vinst-förhållandet för Star Wars, Elm Street och The Terminator

Vissa filmer får uppföljare och spin offs till synes utan ände. Inte sällan efter en framgångsrik start, verkar bolagen vara redo att skjuta till mer pengar för att krama mer ur det varumärke som utgör en framgångsrik film. Här är pengarna som satsats och spelats in från tre kända filmserier.

Star Wars, inspelningsbudget och bruttointäkt (miljoner dollar):

Förhållande, inspelningsbudget och bruttointäkt:

Gissningsvis kommer den näst sista filmen dra in lite mer pengar över åren, och för den sista filmen finns ingen uppgift om intäkter än. Förmodligen kommer den vara mer lönsam i förhållande till vad den kostat än de två tidigare filmerna.

Terror på Elm Street, inspelningsbudget och bruttointäkt (miljoner dollar):

Förhållande, inspelningsbudget och bruttointäkt:

I den sista filmen, som är en nyinspelning av den första, är det inte Robert Englund som spelar Freddy Krueger.

The Terminator, inspelningsbudget och bruttointäkt (miljoner dollar):

Förhållande, inspelningsbudget och bruttointäkt:

Så troligen är det ett bra förhållande mellan satsade och tjänade pengar som avgör hur många uppföljare en film får.

Del 2.

Metabollar

Jag tänkte visa en komplett implementation av 2-dimensionella metabollar i C#. Denna teknik skulle även kunna användas i 3D. Effekten ser ut så här:

Effekten beskrivs här, och denna implementation prioriterar prestanda för att fungera i realtid. I videon ovan används 20 positiva bollar (alltså bollar som tenderar att smeta ihop) och 10 negativa bollar (alltså bollar som tenderar att sluka andra bollar) och samtliga rör sig i sinusbanor på en yta av 320×200 pixlar. Renderingen bygger på Windows Forms och klassen Bitmap.

För att få rörelserna mjuka har jag satt formulärets egenskap DoubleBuffered till true. Dessutom har jag placerat en Timer på formuläret och satt dess Interval till 1 för att få så bra prestanda som möjligt utan att köra dedikerat, men om man ställer ännu högre krav på användarupplevelsen, skulle man välja ett annat bibliotek än Windows Forms. Dessa inställningar har jag ställt in i Visual Studio, så de syns inte i koden.

Här är formulärets kod:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace Metaballs
{
    public partial class Form1 : Form
    {
        private IRenderer _renderer;
        private Bitmap _bitmap;
        private int[,] _pixels;
        private double _counter = 0.0;
        private readonly List<Ball> _balls = new List<Ball>();
        
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Shown(object sender, EventArgs e)
        {
            Refresh();
            _renderer = new MetaRenderer();
            _bitmap = new Bitmap(320, 200);
            _pixels = new int[320, 200];
            for (var i = 0; i < 30; i += 3)
            {
                _balls.Add(new Ball(MovementDescription.Randomize(), i, false));
                _balls.Add(new Ball(MovementDescription.Randomize(), i + 1, false));
                _balls.Add(new Ball(MovementDescription.Randomize(), i + 2, true));
            }
            timer1.Enabled = true;
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            //Clear array.
            for (var y = 0; y < 200; y++)
                for (var x = 0; x < 320; x++)
                    _pixels[x, y] = 0;

            foreach (var ball in _balls)
                _renderer.Apply(ball, _pixels);
            
            //Draw.
            _renderer.Draw(_pixels, _bitmap);
            foreach (var ball in _balls)
                ball.Tick(_counter);

            _counter += 1.0;
            Refresh();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            if (_bitmap == null)
                return;
            e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
            e.Graphics.DrawImage(_bitmap, 0, 0, 640, 400);
        }
    }
}

De resurser som används är _renderer som ansvarar för att bestämma hur bollarna påverkar varandra och hur det ska se ut, _bitmap som är ytan jag renderar till, _pixels som är min matris för beräkningar, _counter som används för sinusbanorna och _balls som är mina metabollar. Notera att jag, likt inställningarna jag nämnde tidigare, låtit Visual Studio mappa upp mina events, så dessa syns inte i koden. Men de kan läsas av från namngivningen av procedurerna.

Som synes av den sista kodraden zoomar jag resultatet med 50%. Om man vill se resultatet i naturlig storlek behöver man inte ange InterpolationMode (efter som ingen pixelinterpolering då sker) eller någon ny storlek (640, 400).

Jag har valt att peka ut renderingen med ett interface (IRenderer) eftersom det är mycket troligt att man vill ha olika renderingskod under tiden man designar sitt alster och när man är nöjd. T.ex. är det rimligt att man tydligt vill se sina bollar flytta sig över skärmen när man kalibrerar deras banor, men när det är färdigt vill man självklart se hur de påverkar varandra, eftersom det är det som är syftet (och det som visas i videon ovan). Här är IRenderer:

using System.Drawing;

namespace Metaballs
{
    public interface IRenderer
    {
        void Apply(Ball source, int[,] target);
        void Draw(int[,] source, Bitmap target);
    }
}

En debug-implementation kanske skulle låta Apply färgsätta de individuella bollarna olika och låta Draw visa det. Däremot är en produktionsimplementation av IRenderer hjärtat i programmet. Där skulle Apply addera eller subtrahera en boll på spelplanen, och Draw skulle visa en kontur baserat på det. En boll (Ball) representerar en position och en rörelse för en boll, och för att kunna hitta tröskelvärden när två bollar är nära varandra, består dessa av en cirkel med låga värden nära konturen och höga värden nära centrum. På så vis kan en godtycklig kontur skapas från summan som skapas när bollarna adderats till ytan (eller subtraherats, för de negativa bollarna). I min implementation är varje boll 98×98 pixlar stor, varför deras centrum ligger 49 pixlar i varje riktning från sin position, vilket framgår av Apply:

using System.Drawing;

namespace Metaballs
{
    public class MetaRenderer : IRenderer
    {
        private Color[] _colors;

        public MetaRenderer()
        {
            _colors = new Color[100];
            for (var i = 0; i < _colors.Length; i++)
                _colors[i] = Color.Black;
            var j = 60;
            for (var c = 0; c < 255; c += 30)
            {
                _colors[j] = Color.FromArgb(c, c, c);
                j++;
            }
            for (var c = 255; c >= 0; c -= 30)
            {
                _colors[j] = Color.FromArgb(c, c, c);
                j++;
            }
        }

        public void Apply(Ball source, int[,] target)
        {
            var ballX = (int)source.X - 49;
            var ballY = (int)source.Y - 49;
            for (var y = 0; y < 98; y++)
            {
                for (var x = 0; x < 98; x++)
                {
                    var xp = ballX + x;
                    if (xp < 0 || xp >= 320)
                        continue;
                    var yp = ballY + y;
                    if (yp < 0 || yp >= 200)
                        continue;
                    if (source.Negative)
                        target[xp, yp] -= DataDefinitions.Ball98[x, y];
                    else
                        target[xp, yp] += DataDefinitions.Ball98[x, y];
                }
            }
        }
        
        public void Draw(int[,] source, Bitmap target)
        {
            for (var y = 0; y < 200; y++)
            {
                for (var x = 0; x < 320; x++)
                {
                    target.SetPixel(x, y,
                        source[x, y] < _colors.Length
                        && source[x, y] >= 0
                            ? _colors[source[x, y]]
                            : Color.Black);
                }
            }
        }
    }
}

(Anti-aliasing-effekten uppnås av den toning som konfigureras i konstruktorn ovan.) Så här ser en boll ut:

using System;
   
   namespace Metaballs
   {
       public class Ball
       {
           private const double CenterX = 159.0;
           private const double CenterY = 99.0;
           private readonly MovementDescription _d;
           
           public double X { get; private set; }
           public double Y { get; private set; }
           public int Index { get; }
           public bool Negative { get; }
           
           public Ball(MovementDescription movementDescription, int index, bool negative)
           {
               _d = movementDescription;
               Index = index;
               Negative = negative;
               X = 159;
               Y = 99;
           }
   
           public void Tick(double counter)
           {
               X = CenterX + Math.Sin(counter / _d.SinFactor1) * _d.SinRadius1
                           + Math.Sin(counter / _d.SinFactor2) * _d.SinRadius2;
               Y = CenterY + Math.Cos(counter / _d.CosFactor1) * _d.CosRadius1
                           + Math.Cos(counter / _d.CosFactor2) * _d.CosRadius2;
           }
       }
   }

MovementDescription är en struktur som samlar data om hur sinusbanan ska se ut. Som man kunde ana från huvudprogrammet erbjuder strukturen en möjlighet att låta slumpen avgöra hastighet och radie.

using System;

namespace Metaballs
{
    public struct MovementDescription
    {
        private static Random Rnd;
        public double SinFactor1 { get; }
        public double SinFactor2 { get; }
        public double CosFactor1 { get; }
        public double CosFactor2 { get; }
        public double SinRadius1 { get; }
        public double SinRadius2 { get; }
        public double CosRadius1 { get; }
        public double CosRadius2 { get; }

        
        static MovementDescription()
        {
            Rnd = new Random();
        }
        
        public MovementDescription(
            double sinFactor1,
            double sinFactor2,
            double cosFactor1,
            double cosFactor2,
            double sinRadius1,
            double sinRadius2,
            double cosRadius1,
            double cosRadius2)
        {
            SinFactor1 = sinFactor1;
            SinFactor2 = sinFactor2;
            CosFactor1 = cosFactor1;
            CosFactor2 = cosFactor2;
            SinRadius1 = sinRadius1;
            SinRadius2 = sinRadius2;
            CosRadius1 = cosRadius1;
            CosRadius2 = cosRadius2;
        }

        public static MovementDescription Randomize() =>
            new MovementDescription(
                Rnd.Next(10, 100),
                Rnd.Next(10, 100),
                Rnd.Next(10, 100),
                Rnd.Next(10, 100),
                Rnd.Next(10, 100),
                Rnd.Next(10, 100),
                Rnd.Next(10, 100),
                Rnd.Next(10, 100)
            );
    }
}

Därmed är det enda som återstår själva bollen som ovan refereras till som Ball98, uppkallad efter sina sidors storlek. För att göra det enkelt, ritade jag boll genom att välja penselverktyget i Photoshop och skrev ett enkelt program som konverterade det hela till en int array i C#, enligt följande:

namespace Metaballs
{
    public class DataDefinitions
    {
        public static int[,] Ball98 = new int[98, 98]
{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 2, 2, 3, 3, 2, 3, 2, 3, 3, 3, 3, 4, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 4, 4, 4, 4, 5, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 4, 3, 4, 3, 4, 5, 5, 5, 6, 5, 5, 6, 6, 6, 6, 5, 5, 6, 6, 6, 6, 5, 5, 4, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 3, 2, 2, 1, 1, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 5, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 5, 5, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 7, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 6, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 6, 7, 6, 7, 8, 9, 9, 9, 9, 9, 10, 10, 9, 9, 10, 10, 10, 9, 9, 10, 9, 9, 8, 9, 8, 8, 8, 7, 7, 6, 6, 5, 4, 5, 4, 4, 3, 2, 3, 3, 3, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 8, 9, 10, 10, 11, 11, 12, 12, 12, 12, 12, 12, 12, 13, 12, 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 8, 7, 7, 6, 6, 5, 5, 4, 4, 4, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 5, 5, 6, 6, 6, 8, 8, 9, 9, 10, 10, 11, 12, 12, 13, 13, 14, 14, 14, 15, 15, 14, 14, 15, 15, 14, 14, 13, 13, 13, 12, 11, 12, 11, 10, 9, 8, 8, 7, 6, 6, 5, 5, 4, 5, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 6, 7, 8, 8, 10, 10, 11, 11, 12, 12, 13, 14, 14, 15, 15, 16, 16, 16, 17, 17, 17, 17, 17, 17, 16, 16, 16, 15, 15, 14, 14, 14, 13, 11, 11, 10, 10, 9, 8, 8, 7, 7, 6, 5, 5, 4, 4, 3, 3, 3, 3, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 6, 7, 7, 8, 9, 9, 11, 11, 12, 13, 14, 15, 16, 16, 17, 18, 19, 19, 20, 20, 20, 20, 21, 20, 20, 20, 20, 19, 19, 18, 18, 17, 16, 16, 15, 14, 13, 12, 12, 11, 9, 9, 8, 7, 6, 6, 5, 4, 4, 3, 3, 3, 3, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 3, 4, 4, 5, 5, 6, 7, 8, 8, 9, 10, 11, 13, 13, 14, 15, 16, 16, 17, 19, 21, 21, 22, 22, 22, 22, 23, 23, 24, 24, 24, 23, 23, 22, 21, 21, 21, 19, 19, 19, 18, 17, 14, 15, 14, 12, 11, 10, 9, 8, 7, 7, 5, 5, 5, 4, 4, 3, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 4, 5, 6, 7, 7, 7, 9, 11, 11, 12, 13, 15, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 25, 26, 26, 27, 27, 28, 27, 27, 27, 27, 26, 25, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 14, 13, 12, 11, 11, 9, 7, 8, 6, 6, 6, 5, 4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 7, 7, 9, 9, 10, 11, 13, 14, 15, 17, 18, 19, 21, 23, 23, 25, 25, 26, 28, 28, 29, 31, 31, 31, 32, 32, 31, 31, 31, 31, 31, 30, 29, 28, 26, 25, 25, 23, 22, 21, 19, 18, 17, 15, 14, 13, 11, 11, 10, 8, 8, 7, 6, 6, 5, 4, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 5, 5, 6, 7, 8, 9, 11, 12, 13, 15, 16, 18, 20, 20, 21, 23, 26, 26, 28, 29, 30, 32, 33, 33, 35, 35, 36, 36, 36, 36, 36, 36, 35, 35, 34, 33, 32, 30, 29, 28, 27, 25, 23, 21, 21, 20, 18, 16, 15, 13, 12, 11, 9, 8, 8, 6, 6, 5, 4, 4, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 17, 18, 20, 22, 24, 25, 27, 29, 30, 32, 34, 35, 37, 37, 39, 40, 41, 42, 42, 42, 42, 42, 42, 41, 40, 38, 37, 36, 35, 33, 32, 30, 29, 27, 25, 23, 22, 20, 18, 17, 15, 14, 13, 11, 10, 9, 8, 7, 6, 5, 4, 4, 4, 3, 3, 2, 1, 2, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 5, 5, 7, 8, 10, 10, 11, 12, 14, 16, 17, 19, 22, 24, 26, 27, 29, 31, 33, 35, 37, 39, 40, 42, 43, 44, 45, 46, 47, 48, 48, 48, 48, 47, 46, 46, 44, 44, 42, 40, 39, 37, 35, 33, 31, 29, 27, 25, 23, 21, 20, 18, 16, 15, 13, 11, 10, 9, 8, 7, 5, 5, 5, 4, 3, 3, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 5, 7, 7, 8, 9, 11, 13, 14, 16, 18, 20, 22, 25, 27, 29, 30, 33, 35, 37, 40, 42, 43, 45, 47, 49, 50, 52, 53, 53, 54, 54, 54, 54, 53, 52, 52, 50, 49, 47, 45, 44, 41, 39, 38, 35, 33, 30, 29, 27, 25, 22, 20, 18, 16, 14, 13, 11, 9, 8, 7, 7, 5, 5, 5, 4, 3, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 4, 4, 4, 6, 7, 7, 9, 10, 11, 13, 15, 16, 18, 20, 23, 25, 27, 30, 32, 35, 38, 40, 43, 45, 47, 49, 51, 53, 55, 56, 58, 59, 60, 60, 60, 60, 60, 59, 59, 58, 56, 55, 53, 51, 50, 47, 45, 42, 40, 38, 35, 32, 30, 27, 24, 22, 20, 18, 16, 15, 13, 11, 10, 9, 7, 7, 6, 4, 4, 3, 3, 2, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 6, 7, 9, 10, 11, 12, 15, 17, 19, 21, 23, 26, 29, 31, 34, 36, 39, 42, 45, 48, 50, 52, 56, 58, 60, 62, 63, 65, 66, 67, 68, 68, 68, 68, 67, 66, 66, 64, 62, 60, 58, 55, 53, 51, 48, 44, 42, 39, 36, 34, 30, 28, 26, 23, 21, 19, 16, 14, 12, 11, 10, 9, 7, 6, 5, 5, 4, 4, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 3, 3, 4, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 18, 21, 24, 26, 29, 32, 34, 37, 40, 43, 47, 49, 53, 56, 59, 62, 65, 67, 69, 71, 73, 74, 75, 76, 76, 76, 76, 75, 74, 73, 71, 69, 67, 65, 62, 59, 55, 53, 50, 47, 43, 40, 37, 34, 32, 29, 26, 24, 21, 18, 17, 14, 12, 11, 9, 7, 7, 6, 5, 4, 4, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3, 4, 5, 6, 7, 8, 9, 11, 13, 14, 17, 18, 20, 23, 26, 29, 32, 34, 38, 42, 45, 48, 52, 55, 59, 62, 65, 68, 72, 74, 77, 79, 81, 82, 83, 84, 84, 85, 84, 83, 82, 81, 79, 76, 74, 72, 69, 65, 61, 59, 56, 52, 49, 45, 41, 38, 35, 32, 28, 26, 23, 20, 19, 16, 14, 13, 10, 9, 8, 6, 6, 5, 5, 3, 3, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 6, 8, 9, 10, 12, 14, 16, 18, 21, 23, 26, 29, 32, 35, 39, 42, 46, 49, 53, 57, 61, 65, 68, 72, 76, 79, 82, 85, 87, 89, 91, 92, 93, 93, 93, 93, 91, 91, 89, 86, 84, 82, 79, 76, 72, 69, 65, 61, 57, 54, 49, 45, 41, 38, 35, 32, 29, 26, 23, 21, 18, 15, 14, 11, 10, 9, 8, 6, 5, 5, 4, 3, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 4, 5, 6, 7, 9, 9, 11, 12, 15, 17, 20, 23, 25, 28, 32, 35, 39, 42, 46, 50, 54, 59, 63, 67, 71, 75, 80, 84, 87, 90, 93, 96, 98, 99, 100, 102, 102, 103, 102, 100, 99, 98, 96, 93, 90, 87, 83, 79, 75, 71, 67, 63, 59, 54, 51, 46, 42, 38, 35, 32, 28, 25, 23, 20, 17, 15, 12, 11, 10, 8, 7, 6, 5, 4, 4, 3, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0}, 
{0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 4, 4, 5, 6, 7, 8, 10, 11, 13, 14, 17, 19, 22, 25, 27, 30, 35, 39, 42, 46, 50, 56, 60, 65, 69, 73, 78, 83, 87, 92, 96, 99, 102, 106, 108, 110, 111, 113, 113, 113, 113, 111, 109, 107, 105, 102, 99, 95, 91, 87, 82, 78, 74, 70, 64, 60, 56, 50, 46, 42, 38, 34, 30, 27, 25, 22, 19, 17, 14, 13, 11, 10, 8, 7, 6, 5, 4, 4, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0}, 
{0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 4, 4, 5, 6, 7, 9, 11, 11, 13, 16, 18, 21, 24, 27, 30, 34, 37, 42, 46, 50, 55, 61, 65, 70, 75, 80, 85, 90, 95, 100, 104, 107, 111, 115, 117, 120, 121, 122, 122, 122, 122, 121, 119, 117, 114, 111, 107, 104, 99, 94, 90, 85, 80, 75, 70, 65, 61, 55, 50, 46, 42, 37, 33, 30, 27, 24, 21, 18, 16, 13, 12, 10, 8, 7, 6, 5, 4, 4, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0}, 
{0, 0, 1, 1, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13, 15, 17, 20, 23, 26, 29, 33, 37, 40, 45, 50, 54, 60, 65, 70, 76, 83, 88, 92, 98, 103, 108, 112, 117, 121, 125, 127, 130, 131, 132, 133, 133, 132, 131, 130, 127, 125, 121, 117, 112, 108, 103, 97, 92, 87, 81, 77, 72, 66, 59, 54, 50, 45, 42, 37, 33, 30, 26, 23, 20, 17, 14, 12, 11, 9, 8, 7, 5, 5, 4, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0}, 
{0, 1, 1, 1, 1, 2, 2, 2, 3, 4, 5, 5, 6, 7, 9, 10, 12, 14, 16, 19, 22, 25, 28, 31, 34, 40, 45, 49, 54, 60, 65, 70, 76, 82, 88, 94, 100, 106, 112, 116, 121, 126, 130, 134, 137, 140, 141, 143, 144, 144, 143, 141, 140, 137, 134, 130, 125, 121, 117, 112, 106, 100, 95, 89, 82, 76, 71, 64, 59, 54, 49, 44, 40, 36, 32, 28, 25, 22, 19, 16, 14, 13, 11, 9, 8, 6, 5, 4, 4, 3, 2, 2, 1, 1, 1, 1, 0, 0}, 
{0, 0, 1, 1, 1, 2, 2, 2, 3, 4, 5, 5, 6, 8, 10, 11, 12, 15, 17, 20, 23, 27, 30, 33, 38, 42, 47, 52, 58, 64, 70, 76, 83, 88, 94, 101, 108, 114, 120, 125, 131, 135, 139, 144, 148, 150, 152, 154, 154, 154, 154, 152, 150, 147, 143, 139, 135, 131, 125, 120, 114, 108, 101, 94, 88, 81, 75, 70, 64, 58, 52, 47, 43, 38, 34, 30, 27, 23, 20, 17, 15, 13, 11, 9, 7, 7, 5, 4, 4, 3, 2, 2, 2, 2, 1, 1, 0, 0}, 
{0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 7, 9, 10, 11, 13, 16, 18, 22, 25, 28, 33, 37, 41, 46, 51, 56, 62, 68, 74, 81, 89, 94, 101, 108, 115, 121, 127, 134, 139, 144, 150, 154, 158, 160, 162, 164, 164, 164, 164, 162, 160, 157, 154, 150, 144, 139, 134, 129, 121, 115, 108, 101, 95, 89, 81, 74, 68, 62, 56, 51, 46, 41, 37, 33, 29, 25, 21, 18, 16, 13, 11, 10, 8, 7, 5, 4, 4, 4, 3, 2, 2, 1, 1, 1, 0, 0}, 
{0, 0, 1, 1, 1, 2, 2, 3, 4, 4, 6, 6, 7, 9, 11, 13, 15, 18, 20, 24, 27, 30, 35, 38, 43, 49, 54, 60, 66, 72, 79, 86, 94, 101, 108, 115, 122, 129, 136, 143, 148, 153, 159, 163, 167, 171, 173, 175, 175, 175, 175, 173, 171, 167, 164, 160, 154, 148, 143, 136, 130, 122, 115, 108, 101, 94, 86, 79, 73, 66, 60, 55, 49, 44, 39, 34, 31, 27, 23, 20, 18, 15, 12, 11, 10, 8, 6, 5, 4, 3, 3, 2, 2, 2, 1, 1, 1, 0}, 
{0, 0, 1, 1, 2, 2, 2, 3, 4, 4, 6, 7, 8, 10, 12, 14, 16, 18, 21, 24, 27, 32, 37, 40, 46, 52, 58, 63, 70, 77, 84, 93, 99, 106, 114, 121, 129, 137, 144, 151, 158, 164, 169, 173, 177, 181, 183, 185, 186, 186, 186, 184, 181, 178, 174, 170, 164, 158, 151, 144, 137, 130, 121, 114, 106, 99, 92, 84, 77, 70, 63, 57, 52, 47, 41, 37, 32, 27, 24, 21, 18, 16, 13, 11, 10, 8, 6, 6, 4, 3, 3, 2, 2, 2, 1, 1, 1, 0}, 
{0, 0, 1, 1, 2, 2, 2, 4, 4, 4, 6, 7, 8, 10, 13, 14, 16, 19, 22, 26, 29, 33, 38, 43, 48, 54, 61, 67, 73, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 159, 165, 173, 178, 183, 187, 191, 193, 195, 196, 196, 195, 193, 190, 187, 183, 178, 173, 167, 160, 152, 144, 136, 128, 120, 112, 104, 96, 88, 80, 74, 68, 61, 55, 49, 43, 38, 34, 30, 26, 22, 19, 17, 15, 12, 10, 8, 7, 6, 5, 4, 3, 2, 2, 2, 1, 1, 1, 1}, 
{0, 1, 1, 1, 2, 2, 2, 4, 4, 5, 7, 7, 9, 11, 13, 15, 17, 21, 24, 28, 31, 35, 40, 46, 51, 57, 64, 71, 77, 84, 92, 101, 110, 118, 127, 135, 143, 152, 160, 167, 174, 181, 187, 192, 196, 200, 203, 205, 206, 206, 205, 203, 200, 197, 192, 187, 181, 174, 167, 159, 152, 143, 135, 126, 118, 109, 101, 94, 85, 77, 71, 64, 57, 51, 47, 40, 35, 32, 28, 24, 20, 18, 16, 13, 11, 9, 8, 7, 5, 4, 3, 3, 2, 2, 1, 1, 1, 0}, 
{0, 1, 1, 1, 2, 2, 2, 4, 4, 5, 7, 7, 9, 11, 13, 16, 18, 21, 25, 29, 33, 37, 42, 47, 53, 59, 67, 74, 80, 89, 96, 105, 114, 122, 132, 140, 149, 159, 166, 174, 181, 189, 195, 200, 205, 208, 211, 214, 215, 214, 214, 212, 209, 205, 200, 195, 189, 181, 174, 166, 159, 149, 140, 131, 123, 114, 105, 98, 89, 80, 73, 66, 59, 53, 47, 42, 37, 32, 29, 24, 21, 18, 15, 13, 11, 9, 8, 7, 5, 4, 4, 3, 2, 2, 1, 1, 1, 0}, 
{0, 1, 1, 1, 2, 2, 3, 4, 4, 5, 7, 8, 10, 12, 14, 16, 18, 22, 26, 28, 33, 38, 44, 49, 55, 62, 69, 76, 84, 93, 100, 109, 118, 127, 136, 146, 155, 164, 173, 181, 188, 195, 202, 208, 213, 217, 220, 222, 223, 223, 222, 220, 217, 213, 208, 202, 195, 188, 181, 173, 164, 155, 146, 136, 127, 118, 109, 100, 93, 84, 76, 69, 61, 56, 49, 43, 38, 33, 29, 25, 22, 18, 16, 14, 12, 10, 8, 7, 5, 5, 4, 3, 2, 2, 1, 1, 1, 1}, 
{1, 1, 1, 2, 2, 2, 3, 4, 5, 5, 7, 8, 10, 12, 15, 16, 20, 23, 27, 31, 35, 40, 45, 51, 57, 64, 72, 80, 88, 95, 103, 113, 122, 132, 141, 152, 161, 169, 179, 187, 195, 202, 209, 215, 220, 224, 227, 229, 230, 230, 229, 226, 224, 220, 215, 209, 202, 195, 187, 178, 170, 161, 151, 141, 131, 122, 113, 104, 96, 87, 79, 72, 64, 57, 51, 46, 40, 35, 31, 27, 23, 20, 16, 14, 12, 10, 9, 8, 6, 5, 4, 3, 2, 2, 2, 1, 1, 0}, 
{1, 1, 1, 2, 2, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 17, 21, 24, 28, 32, 36, 41, 46, 52, 59, 65, 73, 81, 89, 99, 107, 116, 127, 137, 147, 156, 165, 175, 183, 193, 201, 208, 214, 221, 227, 231, 234, 236, 237, 237, 236, 234, 231, 227, 221, 215, 208, 201, 193, 183, 175, 165, 155, 146, 136, 127, 117, 108, 99, 89, 81, 74, 66, 58, 52, 47, 41, 36, 31, 27, 24, 20, 18, 15, 12, 10, 9, 8, 6, 5, 4, 3, 2, 2, 1, 1, 1, 0}, 
{0, 1, 1, 1, 2, 3, 3, 4, 5, 6, 8, 9, 10, 12, 15, 18, 21, 24, 28, 32, 36, 42, 47, 54, 60, 67, 76, 84, 92, 101, 110, 120, 130, 140, 150, 159, 169, 178, 188, 198, 206, 213, 220, 226, 232, 236, 240, 242, 243, 243, 242, 240, 236, 232, 226, 220, 213, 206, 198, 188, 178, 168, 160, 149, 139, 130, 120, 111, 101, 91, 83, 76, 67, 60, 54, 47, 43, 38, 33, 29, 24, 21, 17, 15, 13, 11, 9, 8, 6, 5, 4, 3, 2, 2, 1, 1, 1, 1}, 
{0, 1, 1, 1, 2, 3, 3, 4, 5, 6, 8, 9, 11, 13, 15, 18, 21, 25, 29, 33, 37, 43, 48, 55, 62, 69, 77, 85, 93, 102, 112, 122, 131, 142, 153, 162, 172, 182, 193, 201, 209, 218, 225, 231, 236, 241, 244, 247, 247, 247, 247, 245, 241, 237, 231, 225, 218, 209, 200, 192, 183, 173, 162, 152, 142, 132, 122, 112, 102, 94, 85, 77, 69, 62, 54, 48, 43, 38, 33, 29, 25, 21, 19, 16, 13, 11, 9, 8, 6, 5, 4, 4, 3, 2, 2, 2, 1, 1}, 
{0, 1, 1, 1, 2, 3, 3, 4, 5, 6, 8, 9, 11, 13, 16, 19, 21, 25, 29, 33, 39, 44, 49, 55, 62, 70, 78, 86, 94, 103, 114, 124, 133, 143, 154, 164, 174, 185, 195, 204, 213, 221, 228, 234, 239, 244, 248, 250, 251, 251, 249, 248, 244, 240, 234, 228, 221, 213, 203, 195, 185, 175, 165, 154, 143, 133, 123, 113, 103, 95, 86, 78, 70, 63, 55, 49, 43, 38, 34, 29, 25, 21, 19, 16, 13, 11, 9, 8, 6, 5, 4, 4, 3, 2, 2, 2, 1, 1}, 
{1, 1, 1, 2, 2, 3, 3, 4, 5, 6, 8, 10, 12, 14, 16, 19, 21, 25, 29, 34, 38, 44, 50, 56, 62, 70, 78, 87, 96, 105, 115, 125, 136, 145, 156, 167, 177, 187, 197, 206, 214, 223, 230, 236, 243, 247, 249, 252, 253, 253, 252, 250, 246, 242, 236, 230, 223, 215, 206, 197, 187, 177, 167, 156, 145, 135, 125, 115, 105, 96, 87, 79, 71, 63, 56, 49, 44, 38, 34, 30, 26, 22, 19, 16, 13, 11, 10, 8, 6, 5, 5, 3, 3, 2, 2, 1, 1, 1}, 
{1, 1, 1, 2, 2, 3, 4, 5, 5, 6, 8, 10, 11, 13, 16, 18, 22, 26, 30, 34, 38, 44, 49, 56, 64, 71, 79, 87, 96, 105, 115, 125, 136, 146, 156, 166, 177, 187, 197, 207, 215, 224, 231, 237, 244, 248, 251, 253, 254, 255, 253, 251, 247, 243, 238, 230, 223, 216, 207, 198, 188, 177, 167, 156, 146, 136, 125, 115, 106, 96, 87, 79, 71, 63, 56, 50, 44, 38, 33, 29, 26, 22, 18, 16, 13, 11, 10, 8, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1}, 
{1, 1, 1, 2, 2, 3, 3, 5, 5, 6, 8, 10, 11, 13, 16, 18, 22, 26, 30, 34, 38, 44, 50, 56, 63, 71, 79, 87, 96, 106, 115, 125, 136, 146, 157, 167, 177, 187, 197, 207, 216, 224, 231, 237, 243, 247, 251, 253, 254, 254, 253, 250, 247, 243, 237, 231, 224, 216, 207, 198, 188, 178, 167, 156, 146, 136, 125, 115, 105, 96, 87, 79, 71, 64, 56, 49, 44, 38, 34, 29, 25, 22, 18, 16, 13, 11, 10, 8, 6, 5, 4, 3, 3, 3, 2, 1, 1, 1}, 
{1, 1, 1, 2, 2, 3, 3, 4, 5, 8, 8, 9, 11, 13, 16, 19, 22, 26, 29, 33, 38, 44, 50, 55, 62, 71, 79, 87, 96, 105, 114, 124, 135, 145, 156, 166, 177, 187, 197, 206, 215, 223, 230, 236, 242, 246, 250, 252, 253, 253, 253, 251, 247, 242, 236, 230, 223, 215, 206, 197, 187, 177, 167, 156, 145, 135, 125, 115, 105, 96, 87, 79, 71, 63, 56, 49, 44, 39, 34, 29, 26, 22, 18, 16, 14, 12, 9, 8, 6, 5, 5, 4, 3, 3, 2, 1, 1, 1}, 
{1, 1, 1, 2, 2, 3, 3, 4, 5, 6, 8, 9, 11, 13, 16, 19, 21, 25, 29, 34, 38, 44, 49, 55, 62, 70, 78, 86, 95, 104, 113, 123, 133, 143, 154, 165, 175, 185, 194, 203, 213, 221, 228, 234, 239, 244, 246, 249, 251, 251, 250, 248, 244, 239, 234, 228, 221, 213, 203, 195, 185, 175, 165, 154, 143, 133, 123, 113, 103, 95, 86, 78, 70, 62, 55, 49, 43, 38, 33, 28, 25, 22, 18, 16, 13, 11, 9, 8, 6, 5, 4, 3, 3, 2, 2, 2, 1, 0}, 
{1, 1, 1, 2, 2, 3, 3, 4, 5, 6, 8, 9, 11, 13, 16, 19, 21, 25, 29, 33, 37, 43, 49, 55, 61, 69, 77, 85, 94, 103, 112, 122, 132, 142, 152, 163, 173, 182, 192, 201, 209, 217, 224, 230, 237, 241, 244, 246, 247, 248, 246, 242, 241, 237, 231, 225, 217, 209, 201, 192, 182, 173, 162, 152, 142, 132, 122, 112, 102, 94, 85, 76, 68, 62, 54, 49, 43, 38, 33, 28, 25, 22, 18, 16, 13, 11, 9, 8, 6, 5, 5, 4, 3, 2, 2, 1, 1, 0}, 
{1, 1, 2, 2, 2, 3, 3, 4, 5, 6, 8, 9, 10, 12, 15, 18, 21, 25, 28, 32, 36, 42, 48, 53, 61, 68, 76, 84, 91, 100, 109, 119, 130, 140, 149, 159, 169, 179, 189, 198, 206, 214, 221, 227, 232, 236, 240, 242, 243, 243, 242, 240, 236, 232, 226, 220, 213, 206, 198, 188, 179, 170, 160, 149, 139, 130, 119, 109, 101, 92, 83, 74, 69, 61, 53, 47, 43, 38, 32, 28, 25, 20, 18, 15, 13, 11, 9, 8, 6, 5, 5, 3, 2, 2, 2, 1, 1, 0}, 
{1, 1, 1, 2, 2, 3, 3, 4, 5, 6, 8, 9, 10, 12, 15, 18, 20, 24, 27, 31, 36, 41, 46, 52, 59, 65, 73, 81, 90, 98, 108, 117, 126, 136, 146, 155, 165, 174, 184, 193, 201, 209, 216, 221, 226, 231, 234, 236, 237, 237, 236, 234, 231, 226, 221, 215, 208, 201, 193, 184, 175, 166, 157, 146, 136, 127, 116, 107, 98, 90, 82, 73, 65, 59, 52, 46, 41, 35, 31, 27, 23, 20, 18, 15, 13, 11, 9, 8, 6, 5, 4, 3, 2, 2, 1, 1, 1, 1}, 
{1, 1, 2, 2, 2, 2, 3, 5, 5, 6, 7, 8, 10, 12, 15, 17, 19, 23, 27, 31, 35, 40, 45, 51, 58, 63, 71, 80, 88, 96, 105, 113, 122, 131, 142, 152, 160, 169, 179, 187, 195, 202, 209, 215, 220, 224, 227, 229, 230, 230, 229, 226, 224, 220, 215, 209, 202, 195, 187, 178, 170, 161, 151, 142, 131, 122, 113, 103, 95, 88, 80, 72, 64, 57, 52, 46, 40, 34, 30, 27, 23, 19, 17, 15, 13, 11, 9, 7, 6, 5, 4, 3, 2, 2, 1, 1, 1, 0}, 
{0, 1, 1, 1, 2, 2, 3, 4, 4, 5, 7, 8, 10, 12, 15, 17, 19, 23, 26, 29, 33, 38, 44, 49, 55, 61, 69, 77, 85, 93, 100, 109, 118, 127, 137, 147, 155, 164, 172, 180, 188, 195, 202, 208, 213, 217, 220, 222, 223, 223, 222, 220, 217, 213, 208, 202, 195, 188, 181, 173, 163, 155, 146, 136, 127, 118, 109, 101, 93, 84, 75, 69, 62, 55, 49, 44, 38, 34, 29, 25, 22, 18, 16, 14, 12, 9, 8, 7, 5, 4, 4, 3, 3, 2, 1, 1, 1, 1}, 
{1, 1, 1, 1, 2, 2, 3, 4, 4, 5, 7, 8, 9, 11, 13, 15, 18, 21, 25, 28, 32, 37, 41, 47, 53, 59, 66, 74, 81, 88, 96, 105, 114, 123, 132, 141, 149, 158, 167, 174, 181, 188, 195, 200, 205, 209, 212, 214, 214, 215, 214, 211, 208, 205, 200, 194, 189, 182, 174, 166, 157, 149, 142, 131, 122, 114, 105, 97, 89, 81, 72, 66, 59, 53, 47, 42, 37, 32, 28, 24, 21, 18, 15, 13, 11, 9, 8, 7, 5, 4, 4, 3, 3, 2, 1, 1, 1, 0}, 
{1, 1, 1, 1, 2, 2, 3, 4, 4, 5, 6, 8, 9, 11, 13, 15, 17, 20, 23, 27, 31, 35, 40, 46, 51, 57, 64, 70, 78, 85, 92, 100, 109, 118, 126, 135, 143, 151, 160, 167, 173, 180, 187, 192, 196, 199, 202, 205, 205, 206, 205, 203, 200, 196, 192, 187, 180, 174, 167, 159, 151, 143, 135, 126, 118, 110, 100, 92, 85, 78, 70, 64, 57, 52, 46, 40, 35, 31, 28, 24, 21, 17, 15, 13, 11, 9, 8, 6, 5, 4, 4, 2, 2, 2, 1, 1, 1, 0}, 
{0, 1, 1, 1, 1, 2, 2, 3, 4, 5, 6, 8, 8, 10, 13, 14, 16, 19, 23, 26, 28, 34, 39, 43, 48, 54, 60, 66, 74, 81, 88, 96, 104, 112, 120, 129, 136, 144, 152, 160, 166, 172, 178, 183, 187, 190, 193, 195, 196, 196, 195, 193, 191, 187, 183, 178, 172, 166, 160, 152, 144, 136, 128, 121, 113, 104, 96, 88, 81, 74, 66, 61, 55, 49, 43, 38, 33, 29, 26, 22, 19, 16, 15, 13, 10, 8, 7, 6, 4, 4, 4, 2, 2, 2, 2, 1, 1, 0}, 
{0, 1, 1, 1, 1, 2, 2, 3, 4, 4, 6, 7, 8, 10, 12, 14, 16, 18, 21, 24, 28, 31, 36, 41, 46, 51, 57, 64, 70, 77, 83, 92, 99, 106, 114, 122, 129, 137, 144, 151, 158, 163, 169, 174, 178, 181, 183, 185, 186, 186, 185, 183, 181, 178, 174, 169, 163, 157, 151, 144, 137, 129, 121, 114, 106, 99, 93, 84, 77, 70, 64, 58, 52, 46, 40, 37, 32, 27, 24, 21, 18, 15, 13, 12, 10, 8, 7, 6, 4, 4, 3, 2, 2, 2, 1, 1, 1, 0}, 
{0, 1, 1, 1, 1, 2, 3, 3, 4, 4, 5, 6, 8, 10, 11, 12, 15, 18, 20, 24, 27, 30, 35, 39, 44, 48, 54, 60, 66, 73, 79, 86, 94, 101, 108, 115, 122, 129, 136, 143, 148, 154, 159, 164, 168, 171, 173, 175, 175, 175, 175, 173, 171, 168, 164, 159, 155, 149, 143, 136, 129, 122, 116, 108, 101, 94, 86, 79, 73, 66, 60, 54, 49, 43, 39, 34, 29, 26, 23, 20, 18, 15, 13, 11, 10, 8, 6, 5, 4, 4, 3, 2, 2, 1, 1, 1, 0, 0}, 
{0, 0, 1, 1, 1, 2, 2, 3, 3, 3, 5, 6, 7, 9, 10, 11, 13, 16, 18, 22, 25, 28, 33, 36, 41, 46, 51, 56, 62, 68, 75, 80, 87, 94, 101, 108, 115, 121, 127, 134, 139, 145, 150, 154, 157, 160, 162, 164, 165, 165, 164, 163, 160, 157, 154, 150, 146, 140, 134, 127, 121, 115, 108, 102, 94, 88, 81, 74, 68, 62, 56, 51, 46, 41, 37, 32, 27, 24, 21, 18, 16, 14, 12, 10, 8, 6, 6, 5, 4, 3, 3, 2, 2, 2, 1, 1, 0, 0}, 
{0, 1, 1, 1, 1, 1, 2, 3, 3, 3, 5, 6, 7, 7, 9, 11, 13, 15, 17, 20, 23, 26, 31, 34, 38, 42, 47, 52, 58, 63, 69, 76, 82, 88, 94, 101, 108, 113, 120, 126, 131, 135, 141, 144, 147, 150, 152, 154, 155, 155, 154, 153, 150, 147, 144, 140, 136, 130, 125, 120, 114, 108, 101, 95, 88, 81, 75, 70, 64, 58, 53, 48, 43, 38, 33, 30, 27, 23, 20, 17, 15, 13, 11, 9, 8, 6, 6, 5, 4, 3, 3, 2, 2, 2, 1, 1, 1, 0}, 
{0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 9, 10, 12, 14, 17, 19, 21, 25, 29, 32, 36, 39, 43, 48, 54, 59, 64, 71, 77, 82, 88, 94, 100, 105, 111, 116, 121, 125, 131, 134, 137, 140, 141, 143, 144, 144, 143, 142, 140, 137, 134, 130, 126, 122, 116, 111, 105, 100, 94, 88, 82, 76, 71, 65, 60, 54, 49, 44, 41, 36, 31, 28, 25, 22, 19, 16, 14, 12, 10, 9, 8, 6, 5, 4, 4, 3, 2, 2, 1, 1, 1, 1, 0, 0}, 
{0, 1, 0, 0, 1, 1, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 11, 12, 14, 17, 20, 23, 26, 29, 33, 37, 41, 44, 50, 55, 59, 65, 72, 76, 81, 87, 93, 98, 103, 108, 112, 116, 121, 125, 127, 130, 131, 133, 133, 133, 132, 131, 130, 127, 124, 121, 117, 113, 108, 103, 98, 93, 88, 82, 76, 71, 65, 59, 55, 50, 45, 41, 36, 33, 29, 26, 23, 20, 17, 15, 13, 11, 10, 8, 7, 5, 5, 4, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0}, 
{0, 0, 0, 0, 1, 1, 1, 2, 3, 3, 3, 4, 5, 6, 7, 8, 10, 12, 13, 16, 19, 21, 24, 27, 31, 34, 38, 41, 46, 50, 55, 61, 65, 70, 75, 81, 85, 90, 95, 99, 103, 107, 111, 115, 117, 119, 121, 122, 122, 122, 122, 120, 120, 116, 113, 111, 108, 104, 100, 95, 90, 86, 81, 75, 70, 65, 60, 55, 50, 46, 41, 37, 33, 30, 27, 24, 21, 19, 16, 13, 12, 10, 8, 7, 7, 5, 5, 4, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0}, 
{0, 0, 1, 1, 1, 1, 1, 2, 3, 3, 3, 4, 5, 6, 7, 8, 10, 11, 13, 14, 17, 19, 22, 25, 28, 31, 34, 38, 42, 46, 51, 56, 60, 65, 70, 74, 78, 82, 87, 91, 95, 99, 102, 106, 108, 109, 111, 112, 113, 112, 112, 111, 110, 107, 104, 102, 99, 95, 91, 87, 82, 78, 73, 70, 65, 60, 55, 50, 46, 42, 38, 34, 31, 28, 25, 22, 19, 17, 15, 13, 11, 9, 7, 6, 6, 5, 4, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0}, 
{0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 4, 4, 6, 6, 7, 9, 10, 11, 13, 15, 17, 20, 23, 26, 29, 32, 35, 38, 42, 46, 50, 54, 59, 63, 67, 72, 76, 80, 83, 87, 90, 94, 95, 97, 99, 100, 102, 102, 102, 102, 101, 99, 98, 96, 93, 90, 87, 83, 80, 76, 72, 67, 63, 58, 55, 51, 46, 42, 39, 36, 32, 28, 25, 23, 20, 17, 15, 13, 11, 10, 8, 6, 6, 6, 4, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0}, 
{0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 6, 6, 8, 9, 10, 12, 14, 16, 18, 21, 23, 26, 29, 32, 35, 38, 42, 45, 49, 53, 57, 60, 65, 69, 72, 76, 79, 81, 85, 87, 89, 91, 92, 93, 93, 93, 93, 92, 91, 89, 87, 85, 81, 79, 76, 72, 69, 65, 61, 57, 53, 49, 45, 42, 39, 35, 31, 28, 26, 23, 21, 18, 15, 14, 12, 10, 9, 8, 6, 6, 5, 4, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 3, 3, 3, 5, 6, 6, 7, 8, 10, 12, 13, 14, 17, 18, 20, 23, 26, 28, 31, 35, 38, 41, 45, 48, 52, 55, 59, 62, 65, 68, 72, 74, 76, 79, 81, 82, 83, 84, 84, 85, 84, 83, 83, 81, 79, 77, 73, 71, 69, 65, 62, 59, 55, 52, 48, 45, 41, 38, 35, 32, 28, 25, 23, 21, 19, 16, 14, 13, 11, 9, 8, 7, 5, 5, 5, 4, 3, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 4, 5, 5, 7, 7, 8, 10, 11, 12, 15, 16, 18, 21, 24, 26, 29, 32, 34, 37, 40, 43, 47, 50, 53, 56, 59, 62, 65, 67, 69, 71, 73, 74, 75, 76, 76, 77, 76, 75, 74, 73, 71, 69, 66, 64, 62, 59, 56, 53, 49, 47, 43, 40, 37, 34, 32, 29, 26, 24, 21, 19, 17, 14, 12, 11, 10, 8, 7, 5, 5, 4, 4, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 9, 10, 11, 13, 15, 16, 19, 21, 23, 26, 29, 30, 33, 37, 39, 42, 44, 47, 51, 53, 55, 58, 60, 62, 63, 65, 66, 67, 68, 69, 69, 68, 67, 66, 65, 63, 61, 60, 58, 55, 53, 51, 47, 44, 42, 39, 36, 34, 30, 28, 26, 23, 21, 19, 16, 14, 12, 11, 10, 9, 7, 7, 6, 4, 4, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 4, 4, 5, 5, 6, 8, 9, 10, 11, 14, 15, 16, 18, 20, 22, 24, 27, 30, 32, 34, 38, 40, 43, 46, 48, 49, 51, 53, 55, 56, 58, 59, 59, 61, 61, 61, 60, 59, 59, 58, 56, 55, 53, 51, 50, 47, 45, 43, 40, 38, 34, 32, 30, 27, 25, 23, 20, 19, 17, 15, 13, 11, 10, 9, 8, 7, 6, 5, 4, 4, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 5, 6, 7, 8, 8, 9, 12, 13, 14, 16, 18, 20, 22, 24, 27, 29, 31, 33, 35, 37, 40, 42, 43, 45, 47, 49, 50, 51, 53, 54, 54, 53, 53, 54, 53, 52, 51, 50, 49, 47, 45, 44, 41, 39, 37, 35, 33, 31, 28, 26, 25, 22, 20, 18, 17, 15, 13, 11, 9, 8, 7, 7, 5, 5, 4, 4, 3, 3, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 3, 3, 3, 4, 4, 5, 6, 7, 7, 8, 10, 11, 12, 15, 16, 18, 20, 21, 23, 25, 27, 29, 31, 33, 35, 37, 38, 40, 42, 44, 44, 46, 46, 46, 47, 47, 47, 48, 47, 46, 46, 44, 43, 42, 40, 39, 37, 35, 33, 31, 29, 27, 25, 23, 21, 20, 18, 16, 14, 14, 11, 10, 9, 7, 6, 6, 5, 4, 4, 3, 3, 3, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 4, 4, 4, 6, 6, 7, 8, 9, 10, 11, 13, 14, 15, 17, 18, 20, 22, 24, 25, 27, 29, 31, 33, 34, 35, 37, 38, 39, 41, 41, 41, 42, 42, 42, 42, 42, 40, 39, 40, 38, 37, 35, 34, 33, 31, 29, 27, 25, 23, 22, 20, 19, 17, 15, 14, 12, 11, 10, 9, 8, 7, 6, 5, 4, 4, 3, 3, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 5, 5, 5, 7, 7, 8, 9, 11, 12, 13, 15, 16, 18, 20, 20, 21, 24, 25, 26, 28, 29, 30, 32, 33, 34, 35, 35, 36, 36, 36, 36, 36, 36, 36, 35, 33, 32, 31, 31, 30, 28, 26, 25, 24, 22, 21, 20, 18, 17, 15, 13, 12, 11, 9, 8, 7, 7, 5, 5, 4, 4, 3, 3, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 5, 5, 5, 5, 6, 7, 9, 10, 10, 11, 13, 14, 15, 17, 18, 19, 21, 22, 23, 25, 25, 26, 28, 28, 29, 30, 31, 31, 32, 31, 31, 31, 31, 31, 30, 29, 28, 28, 27, 26, 25, 23, 22, 21, 19, 18, 17, 15, 14, 13, 11, 10, 9, 9, 7, 7, 5, 5, 5, 4, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 7, 8, 8, 8, 10, 11, 12, 13, 15, 16, 16, 18, 19, 20, 22, 22, 23, 23, 25, 26, 26, 27, 27, 27, 27, 27, 27, 27, 26, 26, 25, 25, 24, 23, 22, 21, 20, 19, 18, 16, 15, 14, 13, 12, 11, 10, 9, 9, 8, 7, 6, 5, 4, 4, 4, 3, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 2, 1, 1, 2, 3, 3, 3, 3, 5, 5, 5, 6, 7, 8, 8, 9, 10, 11, 12, 14, 14, 16, 17, 18, 17, 20, 21, 21, 21, 22, 23, 23, 23, 23, 23, 24, 24, 23, 23, 22, 23, 21, 21, 20, 19, 19, 18, 17, 15, 14, 13, 12, 11, 10, 9, 8, 7, 7, 6, 5, 5, 4, 4, 3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 9, 11, 11, 12, 14, 14, 15, 16, 17, 17, 18, 19, 19, 20, 20, 20, 20, 20, 21, 21, 20, 20, 19, 19, 19, 18, 17, 16, 16, 15, 14, 14, 12, 12, 11, 9, 9, 8, 7, 6, 6, 5, 5, 4, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 4, 4, 4, 4, 5, 5, 6, 7, 8, 9, 9, 9, 10, 12, 12, 12, 13, 14, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 16, 16, 16, 15, 14, 14, 14, 12, 12, 11, 10, 10, 9, 8, 7, 7, 6, 5, 5, 5, 4, 4, 3, 3, 3, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 10, 10, 10, 10, 11, 13, 13, 12, 14, 14, 14, 15, 15, 14, 14, 15, 15, 14, 13, 14, 13, 12, 12, 12, 12, 10, 10, 9, 8, 8, 7, 7, 6, 6, 5, 4, 4, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 4, 4, 4, 5, 5, 6, 7, 7, 7, 8, 8, 9, 9, 10, 11, 11, 11, 12, 12, 12, 13, 13, 12, 12, 13, 12, 12, 12, 12, 11, 11, 10, 10, 9, 9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 4, 4, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 4, 4, 3, 4, 6, 6, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9, 10, 10, 9, 10, 11, 10, 10, 11, 10, 10, 10, 10, 9, 9, 9, 9, 8, 7, 7, 7, 6, 5, 4, 5, 5, 4, 3, 3, 4, 3, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 8, 8, 9, 8, 8, 8, 8, 7, 7, 7, 7, 6, 6, 6, 5, 5, 5, 4, 3, 3, 4, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 4, 4, 3, 4, 4, 5, 5, 5, 6, 6, 5, 5, 5, 6, 6, 5, 5, 6, 6, 5, 6, 5, 5, 4, 4, 5, 4, 3, 3, 4, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 4, 3, 4, 4, 4, 4, 5, 5, 5, 4, 4, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 2, 2, 2, 3, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 3, 3, 4, 4, 4, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 2, 2, 2, 3, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
    }
}

Mycket nöje!

Mitt första intryck av Windows Terminal

Sent om sidor lyckades jag installera rätt version av Windows 10 för att få ladda hem förhandsversionen av Windows Terminal, och efter att ha testat den så måste jag säga att jag å ena sidan inte har några stående ovationer att bjuda på, men måste samtidigt säga att det verkligen är hög tid att Windows fick en bra konsol-emulator.

För den som inte är varm i kläderna när det gäller grafiska användargränssnitt (GUI) av WIMP-typ så vill jag påpeka att dessa ofta behöver en konsol-emulator som erbjuder användaren att köra applikationer som inte har något passande grafiskt gränssnitt, utan en enkel kommandotolk, inte minst för att komma åt operativsystemets egna funktioner och tjänster. I Windows 10 handlar det om den gamla CMD, om den lite nyare PowerShell och sedermera även om Bash, men när helst textkommandon ska skrivas i en WIMP-miljö, behövs en konsol-emulator.

Det har funnits lite olika alternativ att välja på i Windows 10, men den enda som Microsoft har erhållit är cmd.exe, som är så pass eftersatt att möjligheten att klippa ut, kopiera och klistra in text är att betrakta som relativt ny funktionalitet. Men det är cmd.exe som använts för den som vill köra CMD, PowerShell eller Bash.

Windows Terminal verkar som sagt inte vara någon revolution (än) men är definitivt ett behövligt tillskott till Windows 10. Fönstret bjuder på ett plus-tecken där man kan öppna en ny flik med någon installerad kommandotolk (vilket endast är CMD och PowerShell om man inte lagt till någon tredjepartstolk).

Ett irritationsmoment i den här förhandsversionen är att du inte kan flytta själva fönstret genom att dra i fönstertiteln utanför PowerShell-fliken, före plus-symbolen. Det är endast området mellan plus-symbolen och minimeringsknappen som kan användas för att positionera fönstret. Det måste väl ändå vara en bugg?

Om man väljer att ändra inställningarna för Windows Terminal startar den editor du valt att använda för JSON, vilket i mitt fall (tydligen) är Visual Studio. Fördelen med att inställningarna är lagrade i INI-filer, XML-filer eller JSON-filer är att det är enkelt att redigera och förvalta dem, men nackdelen är att man får konsultera Bing (eller motsvarande) för att få reda på vad som går att göra. Men eftersom Windows Terminal installeras med stöd för CMD och PowerShell och eftersom jag titt som tätt använder Developer Command Prompt for Visual Studio (DCPVS), så tänker jag att jag vill lägga till den under plus-tecknet.

Inställningsfilen (som av någon anledning heter profiles.json) innehåller ett objekt per flik, så genom att klippa och klistra, och justera så borde man enkelt kunna lägga till DCPVS.

Trodde jag. Men på den punkten får jag inget gensvar. Min nya post visas, men kommandot jag vill exekvera, körs inte.

Det kan såklart handla om skit bakom spakarna, men om så är fallet, så behöver åtminstone jag mer tydlighet. Så för att köra DCPVS måste jag fortfarande snällt köra VsDevCmd.bat manuellt, min konfiguration till trots.

Detta projekt behövs, och jag kör som sagt en förhandsversion av Windows Terminal, men hittills är jag inte jätteimponerad.

Skapa binära filer snabbt och enkelt

Ibland behöver man skapa binära filer, t.ex. för att testa en egenutvecklad file header. För att göra detta kan man starta HxD och peta in de bytes man vill ha i filen. Men om man vill trycka in tal större än 255 eller textsträngar så är det en del att hålla i huvudet. Programmet MkBin bygger en binärfil efter instruktioner i en textfil.

Programmet tar två argument. Först -source som anger textfilen som beskriver binärfilen och -target som anger den binära filen som ska skrivas.

Tal som anges i textfilen antas vara bytes (8-bitarstal mellan 0 och 255), men det går att skriva in kontrollord framför för att ändra datatyp, vilket exemplet i bilden ovan visar.

Om man vill laborera med vilka texter som ger vilket binära resultat, kan man starta programmet med växeln -prompt istället för -source och -target, bara genom att skriva:

MkBin.exe -prompt

Då kan man skriva text och få direkt feedback på vad som skulle hamna i en textfil. Kontrollord som byte, short, int och long, eller 8-bit, 16-bit, 32-bit eller 64-bit anger formatet på efterföljande tal och uttryck som anges inom citat antas vara strängar med UTF-8-kodning. Exempel:

MkBin finns att hämta här.

Sprdef 1.7

Version 1.7 of the Commodore 64 sprite editor for Windows, Sprdef, allows the user to scroll and flip a sprite and fixes a bug with thumbnail updates.

Sprdef version 1.7

Features:

  • Undo/redo buffer
  • Single color/multi color sprite edit
  • CBM prg Studio integration
  • BASIC import/export
  • Keyboard first editing or mouse first editing

Download: http://winsoft.se/files/SetupSprdef.exe

Sprdef requires .NET Framework 4.8 or later.

Skärmbyte på arkadspelet

Jag äger en nytillverkad klassisk argadmaskin som spelar Jamma-kassetter, där skärmen tyvärr gav upp. Som synes klarar arkadspelet antingen vertikala eller horisontella spel med digital styrning. Jag använder den för vertikala spel.

Det var enkelt att skruva loss den gamla skärmen och sätta den nya panelen på plats, men jag upptäckte naturligtvis lite för sent att jag satte den nya panelen 1,5 millimeter för långt till höger.

Framför skärmen sitter två glasskivor med ett motivtryckt papper emellan, för att dölja skärmens kant.

Här är glasskivorna på plats och listen som håller fast dem är fastskruvade. Här såg jag mitt lilla misstag, men beslutade mig för att skruva ihop den ändå, och justera panelens position i sidled vid ett senare tillfälle.

Så här ser spelkassetterna ut. Idag finns även nytillverkade spelkassetter med flera spel på, men typiskt innehåller kassetterna spel från Pac Man- och Galaga-eran. Så här ser den ut med Ms. Pac Man laddad:

Så nu är det bara att stoppa in en femma och köra igång!

Följ mig på Instagram för fler äventyr:

Visa det här inlägget på Instagram

Skärmbyte på arkadspelet!

Ett inlägg delat av Anders Hesselbom (@andershbom)

Sprdef 1.5

Sprdef är en sprite-editor för Windows 10 som används vid spelutveckling för Commodore 64 med Windows som utvecklingsmiljö. Version 1.5 integrerar med CBM prg Studio, har undo/redo-buffer, fungerar i både single- och multicolor-läge och kan importera/exportera BASIC-data. Ladda hem programmet här.

Skärmbild av Sprdef 1.5.