Professor Moji : a javascript educational game in just 1024 bytes

 


INTRO

Hello!

Once again I’m here with one of those long postmortems I use as reference for my future projects, but that can also be useful to other people.
I’ve split it into different sections so you can just skip to the part that interests you the most ;D

This will be some sort of a part 2 of my last year’s post about golfing, which I suggest you to check out because I’m going to reference it a bit.
Well, here, I’ll mainly talk about new tricks I’ve used, new ideas, and about my entry to this year’s JS1024 competition: Professor Moji.

So, my personal challenge last time was to try and create a game in 1024 bytes, thing I never did before, golfing both code and design, and it has been a great experience.
This time, since I’m all focused on working with and studying Serious Games lately, I was all like “yeah, but can I create a serious game in just 1024 bytes?

Could I?
Let’s check it out! :p


PROFESSOR MOJI

Click here to play Professor Moji.

Learn animals names in different languages, and test your skills in a text to speech based “find the object” game.
VOLUME UP!

To change game’s language add a query string at the end of the URL, like “?en” for English or “?ru” for Russian!


If you want you can also click here to play the entry directly from the js1024 competition website.


GAME IDEA

2021’s edition of JS1024 actually had a theme, which was “CREATURE“.
It was optional but I like to follow the theme during competitions of this kind, because it’s a great way to explore cool new ideas 🙂

In this case, “serious games” + “creature” instantly led me to a wide variety of possibilities, like “news game about some animal life cycle”, “effects of pollution on sea creatures” or “learn to classificate animals”.

This combination also reminded me of those sound books for toddlers, to learn letters, animal names and calls, etc. which, for their simplicity, seemed to be the most solid idea for a 1024 bytes project 🙂

To be a game though, a sound book needs to be “gamified“, or it is just a virtual sound book.
And, to be valid as educational, it has to follow specific design guidelines, and to have short and long term assessments on its effects, which is already hard to do with bigger projects.

BUT, what if I could just follow the design of a similar “game” we did in school, when during the listening test, the foreign language professor pronounced a word of an object or animal and we had only 5 seconds to point it out on a board?

Well, this is already “gamified” since it has scores (grades) and it has already an assessment on its educational effects, since it’s a method used in schools.

Worst case scenario, if it doesn’t work well for learning purposes, it can be just considered a “news game”, like “hey do you know how horse is pronounced in Japanese?”, which can work well as a serious game, too.

Thanks for the game mechanic, professor!

And so I decided to create an educational game based on a listening test I did in school, themed on animals, divided in 3 specific screens: 

  • learning screen in which you actually learn the spelling of different animals in the selected language, by clicking on them.
  • test screen in which you test what you learned against time, clicking on the correct animal pronounced by the teacher
  • score screen which is the gamified motivational part, based on challenge and highscores.

An important feature I felt mandatory to add, is language selection.
Binding Professor Moji to a single language really seemed too limiting.

Having a cool drop down list or different flags to click would have been great, but it’s really too expensive in terms of bytes, for just a language selection, in this case.

So inspired by other js1k and js13k games entries, I’ve opted for implementing it through an URL parameter 🙂
To change game’s language add a query string at the end of the URL, like “?en” for English or “?ru” for Russian!

As a default language I’ve chosen Japanese, because it comes from a way different language root that mine, and I wanted to test on myself if the game actually worked. After hours of testing it seems it does :p

Another important feature for an educational game its accessibility, for reach, and so its compatibility.
I focused a lot in making it playable on mobile and on different screen sizes and orientations , making its font size and element positioning as adaptable as possible.
This is definitely a point to improve in Professor Moji, because it resulted in a awkward clicking behaviour (it works best on mobile) and in a not-so-precise layout (again, works better on mobile).

A cool hidden feature I decided to add is about the background color.
Why is it blue?
To not leave anything to chance, and inspired by things like “Steiner education”, I wanted the background color to contribute to the educational purpose of my game, too 🙂
And so I made it blue, because according to this study it seems that “using a blue background increases the likelihood that the information delivered will be remembered”.
More on impact of colors in learning here.

Also, obviously, there are some features that I had to leave out due to lack of bytes, like “written pronunciation” and “written animal names” that I think would have greatly added to the learning scope . Maybe next time 🙂

Noticed anything strange?
Indeed.
Professor Moji is built along the lines of Detective Moji 😮😮😮

After last year’s js1024, I’ve thought about improving Detective Moji, like optimizing it more, adding sounds, correcting some mistakes etc. but I actually never did it.
What better occasion than this?

If you read my previous post,  you know that I wanted
to go a little further, adding some character. A game, to be a better game, needs to create a feeling of identification within the player.
This is usually obtained by adding context to the mechanics, a setting, a background story, and some characters to empathize with.
And so I added the emoji in the title screen, an adequate description and game name to obtain this in just 1024 bytes.

As for Detective Moji, having a “professor emoji” in the title screen gives context, making the player label himself as a “student”, which can help the learning effect, too.

With Professor Moji though, I wanted to enhance this even more, by adding more to the setting,  to the story, creating a “universe“, by using the same screen setup, the same find-the-object game mechanics and a similar game name based on a surname.

And so the story, the narrative, becomes something like this:
What if Detective Moji had a relative, using his same find-the-object skills, not to make identikits, but for teaching in a school?

Practically I created a “Moji Universe” by making a Detective Moji sequel!

Yeah, it is barely noticeable, but I’m pretty proud of this “game design golfing” made of small details and with big care 😀

Summarizing:
“Professor Moji” is an attempt to create an educational game in 1024 bytes and yes, it is kind of a sequel of my 2020 entry “Detective Moji” 😀
The “find the object” game mechanic must be strong in the Moji family!

And that’s it for the design part.
Let’s get in to the techical details now! 🙂

 


CODE STRUCTURE

The code structure is as follows:

  • var declaration
  • update function
    update/draw
  • click listener
  • game over function
    cannot be anonymous because it needs to be called from both update and listener
  • TTS function
    cannot be anonymous because it needs to be called from both update and listener
  • generate emoji function (restart level)
    cannot be anonymous because it needs to be called from itself and click

READABLE CODE

This year I don’t have a high level readable code, but I commented it at best, so it should be readable enough:

/*
Professor Moji
Mattia Fortunati 2021
https://www.mattiafortunati.com/
*/

/*
 Remember, from the shim we have:
 a, // canvas
 b, // body
 c, // 2D canvas context
*/


/*
VARIABLES DECLARATION
*/
for (Z in
    t = 9, //MAX TIME
    U = L = 0, //CURRENT STATE, BEST SCORE
    w = a.width, //GAME WIDTH
    h = a.height, //GAME HEIGHT
    F = (w < h ? w : h) * .1, //FONT SIZE , equivalent to Math.min(w, h)
    c.font = F / 13 + 'em"', //setting font for canvas
    ù = [...'🐖🦌🐘🐇🐍🐬🐡🐝🐒🐕🐈🐎🐄🦔🐓'], //emoji array
    p = [], //Objects array, containing all emoji Qjects. '15' around the code is p.length hardcoded!
    X = w / 2 - F * .5, //setting values for center element
    Y = h * .15, //setting values for center element
    R = Math.random, //setting random since it is used a lot
    M = new SpeechSynthesisUtterance(), //tts message setup
    //setting up language, getting it from URL query string (lik "?en" for english) or setting japanese as default
    M.lang = (self.location.search.substring(1)) || "ja",
    c) c[Z[0] + (Z[6] || Z[2])] = c[Z];
//for(Z in c)c[Z[0]+(Z[6]||Z[2])]=c[Z];
//is a little great hack
//inside of which we can also put all the setters



/*
UPDATE / DRAW FUNCTION
*/
setInterval(x => {
    //Set bg color and set back black color for texts.
    c.fillStyle = '#ADD8E6'
    c.fc(0, 0, w, h)
    c.fillStyle = '#000'

    //draw center element based on state
    c.fx(['👩‍🏫', '❓', '🏁'][U], X, Y)

    //handling states
    //if state not 2
    2 ^ U ? (
            //draw Qjects
            p.find(à => c.fx(à.g, à.x, à.y)),
            //if state is 0
            1 ^ U ?
            //draw the play button
            c.fx('▶️', X, h * .9) :
            //if state is 1
            (
                //draw score and time texts
                //since I had extra characters I do two different draw calls :)
                //c.fx('⭐ ' + S + ' ⏱️ ' + T, X / 2, h * .9),
                c.fx('⭐ ' + S, w / 4 - F, h * .9),
                c.fx('⏱️ ' + T, 3 * w / 4 - F, h * .9),
                //decrease seconds and check for time over
                (B++ % 30 == 0 && T-- < 1) ? G() : 0
            )
        ) :
        //finally if state is 2
        //draw score and best score texts
        //since I had extra characters I do two different draw calls :)
        //c.fx('⭐ ' + S + '  👑 ' + L, X / 2, h / 2)
        (c.fx('⭐ ' + S, X * 0.9, h / 2.2),
            c.fx('👑 ' + L, X * 0.9, h / 1.5))

}, 30)

/*
CLICK LISTENER
*/
a.onclick = e => (
    //stores the object below click
    Q = p.find(x => Math.hypot(x.x - e.x, x.y - e.y) < F * 1.5),
    //if state not 2
    2 ^ U ? (
        //if state is 0
        1 ^ U ? (
            //if an object is actually clicked
            //tts play its graphic
            (Q) ? (M.text = Q.g, l()) :
            //otherwise, go to next state
            (
                U++, //set state 1 (if not 2 and not 1 so its is 0 and U++ is 1)
                S = B = 0, //initialize/reset score and time counter
                T = t, //set time to max time
                j() //generate emojis
            )
        ) :
        //if state is 1
        //if an object is clicked and it's the right one
        ((Q) && Q.g == ì) ? (
            //increase score and generate next level
            S++, //increase score
            T = T < 5 ? 5 : t - S, //update time based on current score (minimum 2)
            B = 0, //update time counter
            j() //generate emojis
        ) :
        //otherwise, game over! (even if click outside of emojis)
        G()
    ) : U = 0 //if state 2 go to state 0
);



/*
GAME OVER
*/
G = x => (L = S > L ? S : L, U = 2) //update best score if needed, change state

//speak
l = x => self.speechSynthesis.speak(M) //speak that M



/*
GENERATE EMOJI ARRAY
*/
j = x => {


    
    //shuffle emoji array
    //from: 
    //https://codegolf.stackexchange.com/questions/45302/random-golf-of-the-day-1-shuffle-an-array
    //15 is ù.length hardcoded :D
    ù.map((v, i) => ù[ù[i] = ù[O = 0 | i + R() * (15 - i)], O] = v)

    //I could have milked it, too, the shuffling itself 
    //is not so important, but in the end I kept the shuffle
    //milked:
    //https://codegolf.stackexchange.com/questions/207456/hot-moo-shuffle-milk-an-array
    //n=15
    //ù=ù.map((_,k)=>ù[n+++n%2*(~k-k)>>1])

    //create Qjects
    _ = F * 1.8 //spacing
    for (i = 15; i--;) p[i] = {
        g: ù[i], //graphic
        x: w / 2 + (i % 5 * _) - _ * 2.4, //x
        y: h / 2 + ((0 | i / 5) * _ - _) //y
    }

    //randomize emoji to pick, and set it as message to speak
    M.text = ì = ù[0 | R() * 15]

    //tts the emoji to pick, unless we are in title screen
    U == 1 ? l() : 0
}

//initialize first screen
j()

GOLFING TECHNIQUES USED

Basically I’ve used all the techniques I’ve learned last year, with much more knowledge, optimizations and improvements, but also something new that I’ll list here.

Arrow Functions

I focused on removing all “functions” declarations, making them arrow functions.

Event Listeners

removed calls to event listeners, which are not needed, since you can just use 

a.onclick = e => ()
Self Instead of Window

Use self instead of window to get location

window.location.search.substring(1)

becomes

self.location.search.substring(1)

 

Comparing

Any logic is reversed comparing for not equal instead of equal.
This makes it possible to use

^

instead of

!= 

saving some bytes

Spread for Array

Use of spread operator to create an array. It improved last year’s emoji array a lot.

ù = '🐖0🦌0🐘0🐇0🐍0🐬0🐡0🐝0🐒0🐕0🐈0🐎0🐄0🦔0🐓'.split(0),

becomes

ù = [...'🐖🦌🐘🐇🐍🐬🐡🐝🐒🐕🐈🐎🐄🦔🐓'],

 

Avoiding Math Calls

Use of

w < h ? w : h 

instead of

Math.min(w, h)

To get the minimum value

Note:
You’ll see that I spent A LOT of bytes for fontsize optimizations and for text positioning.
That’s because, in the end, I had extra bytes, which I decided to use for a better good-looking game layout and so for example I split fillText calls to have 2 different draws, specifically for “score” and “time” in state 1, and 2 different draws for “score” and “best score” in state 2.


GOLFED CODE MINIFIED

As last year, to remove all the comments and further minifying it, I’ve used a web javascript minifier and the code became like this:

for(Z in t=9,U=L=0,w=a.width,h=a.height,F=.1*(w<h?w:h),c.font=F/13+'em"',ù=[..."🐖🦌🐘🐇🐍🐬🐡🐝🐒🐕🐈🐎🐄🦔🐓"],p=[],X=w/2-.5*F,Y=.15*h,R=Math.random,M=new SpeechSynthesisUtterance,M.lang=self.location.search.substring(1)||"ja",c)c[Z[0]+(Z[6]||Z[2])]=c[Z];setInterval(t=>{c.fillStyle="#ADD8E6",c.fc(0,0,w,h),c.fillStyle="#000",c.fx(["👩‍🏫","❓","🏁"][U],X,Y),2^U?(p.find(t=>c.fx(t.g,t.x,t.y)),1^U?c.fx("▶️",X,.9*h):(c.fx("⭐ "+S,w/4-F,.9*h),c.fx("⏱️ "+T,3*w/4-F,.9*h),B++%30==0&&T--<1&&G())):(c.fx("⭐ "+S,.9*X,h/2.2),c.fx("👑 "+L,.9*X,h/1.5))},30),a.onclick=(c=>(Q=p.find(t=>Math.hypot(t.x-c.x,t.y-c.y)<1.5*F),2^U?1^U?Q?(M.text=Q.g,l()):(U++,S=B=0,T=t,j()):Q&&Q.g==ì?(S++,T=T<5?5:t-S,B=0,j()):G():U=0)),G=(t=>(L=S>L?S:L,U=2)),l=(t=>self.speechSynthesis.speak(M)),j=(t=>{for(ù.map((t,c)=>ù[(ù[c]=ù[O=0|c+R()*(15-c)],O)]=t),_=1.8*F,i=15;i--;)p[i]={g:ù[i],x:w/2+i%5*_-2.4*_,y:h/2+((0|i/5)*_-_)};M.text=ì=ù[0|15*R()],1==U&&l()}),j();

 

And if you want you can beautify it back, to make it more readable.
Also, this year, to save like 67 bytes more, I've decided to use RegPack, too!
I suggest you to check RegPack and how it works, because it's really really cool!

The crushed version looks like this:

o=`for(n in t=9,U=IG,w=a.width,h=a.height,D=.1*(w<h?w:hXOont=D/13+'em"',ù=[..."🐖🦌🐘🐇🐍🐬🐡🐝🐒🐕🐈🐎🐄🦔🐓"Fp=[Fk=w/2-.@D,m=.1@h,e=Lrandom,M=new SzUtterance,M.lang=Klocation.search.substring(1)||"jaCc)c[n[0]+(n[6]||n[2])]=c[n];setIntervalZ{qADD8E6COc(0,0,w,hXq000C~["👩‍🏫C"❓C"🏁"][UFk,mX2N(R~t.g,tP)X1N~"▶️CkJ_Y~"⏱️Bf,3*YA++%30=G&&f--<1&&EQ)_V2.2X~"👑BI,V1.5))},30Xa.onclick=(c=>(d=RLhypot(t.x-cP-c.y)<1.@DX2N1Nd?(Td.g,lQ):(U++,S=AG,f=tHd&&d.g==ì?(S++,f=f<5?5:t-S,AGHEQ:UG)XE=Z(I=S>I?S:I,U=2)Xl=ZKsz.speak(M)Xj=Z{for(ù.map((t,c)=>W(Wc]=WbG|c+eQ*(15-c)Fb)]=tXo=1.8*D,i=15;i--;)p[i]={g:WiFx:w/2+i%@o-2.4*o,y:h/2+((0|i/5)*o-o)};Tì=W0|1@eQF1==U&&lQ}XjQ;~Ox(zpeechSynthesisqOillStyle="#_):(~"⭐BS,Z(t=>Yw/4-DJXX),Wù[V.9*k,h/TM.text=Rp.findZQ()P.x,t.yOc.fN^U?LMath.Kself.J,.9*hH,jQ):G=0F],C",B "+@5*`;for(i of`@BCFGHJKLNOPQRTVWXYZ_qz~`)with(o.split(i))o=join(pop());eval(o)

Which is only 927 bytes!

 


NO SHIM VERSION

927 bytes leaves a lot of room for a no shim version 🙂

This code is enough for setting a,b,c as needed:

<canvas id=a><script>c=a.getContext("2d");a.width=innerWidth,a.height=innerHeight,CRUSHED CODE HERE</script>

making the final version of Professor Moji source code, like this:

<canvas id=a><script>c=a.getContext("2d");a.width=innerWidth,a.height=innerHeight,o=`for(n in t=9,U=IG,w=a.width,h=a.height,D=.1*(w<h?w:hXOont=D/13+'em"',ù=[..."🐖🦌🐘🐇🐍🐬🐡🐝🐒🐕🐈🐎🐄🦔🐓"Fp=[Fk=w/2-.@D,m=.1@h,e=Lrandom,M=new SzUtterance,M.lang=Klocation.search.substring(1)||"jaCc)c[n[0]+(n[6]||n[2])]=c[n];setIntervalZ{qADD8E6COc(0,0,w,hXq000C~["👩‍🏫C"❓C"🏁"][UFk,mX2N(R~t.g,tP)X1N~"▶️CkJ_Y~"⏱️Bf,3*YA++%30=G&&f--<1&&EQ)_V2.2X~"👑BI,V1.5))},30Xa.onclick=(c=>(d=RLhypot(t.x-cP-c.y)<1.@DX2N1Nd?(Td.g,lQ):(U++,S=AG,f=tHd&&d.g==ì?(S++,f=f<5?5:t-S,AGHEQ:UG)XE=Z(I=S>I?S:I,U=2)Xl=ZKsz.speak(M)Xj=Z{for(ù.map((t,c)=>W(Wc]=WbG|c+eQ*(15-c)Fb)]=tXo=1.8*D,i=15;i--;)p[i]={g:WiFx:w/2+i%@o-2.4*o,y:h/2+((0|i/5)*o-o)};Tì=W0|1@eQF1==U&&lQ}XjQ;~Ox(zpeechSynthesisqOillStyle="#_):(~"⭐BS,Z(t=>Yw/4-DJXX),Wù[V.9*k,h/TM.text=Rp.findZQ()P.x,t.yOc.fN^U?LMath.Kself.J,.9*hH,jQ):G=0F],C",B "+@5*`;for(i of`@BCFGHJKLNOPQRTVWXYZ_qz~`)with(o.split(i))o=join(pop());eval(o)</script>

Which is pure javascript and only 1018 bytes!

Note:
As last year, in some cases you may need to specify utf8 charset for this to work (due to single letter vars and emojis), also doable from code like this:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

But I’m not doing it in the code directly since it’s not needed in this case, more info here.

 


RESOURCES

If you are interested in javascript golfing, there are a lot of great resources listed on the homepage of  JS1024 competition website, like RegPack, and these tips for golfing in javascript, which I highly recommend since they are a source of immense knowledge on the topic 🙂

Since you are there, don’t forget to check the other entries to JS1024 2021. They are great learning material since each one of them comes with both its minified and readable code!
You can also join the dedicated Discord to interact with the community directly and discover even more resources, like experiments, postmortems, tools etc.

Also, if you are looking for some inspirational, creative, always new, crazy javascript golfing projects I’ll suggest, once again, to follow xem and frank force on Twitter, as well as browsing Dwitter !

 


CONCLUSION

As you can see, Professor Moji may seem like a simple little project, but there’s a lot behind it.
In fact, I must say that it really worked well as a personal tool for learning and experimenting something new, design side and code side. I’m pretty satisfied with the whole experience 🙂
And I’m happy that I found a way, an idea and some dedicated time to participate in this year’s js1024!

Well, thanks for reading my Professor Moji postmortem, I hope you got something interesting from all this sharing of toughts, too, as I always do when reading postmortems. Maybe not what you were looking for, but still something cool 😀

And many many thanks to the js1024 organizers and community for keeping this event up!

Mattia

Leave a Reply

%d bloggers like this: