├── README.md ├── asmle.com ├── asmle.img ├── compile.bat ├── demo.gif ├── dict.py ├── dictionary.bin ├── main.asm ├── makedict.bat └── makedicts.py /README.md: -------------------------------------------------------------------------------- 1 | # ASMLE - Bootable Wordle in 512 bytes 2 | 3 | ![](demo.gif) 4 | 5 | Written in 3 days! (22 bytes left after cleaning up for people) 6 | 7 | ## Running 8 | 9 | DOSBOX (COM version) 10 | 11 | `dosbox asmle.com` 12 | 13 | DOSBOX (Boot sector version) 14 | 15 | `dosbox -c "mount C: ." -c "C:" -c "boot asmle.img"` 16 | 17 | QEMU (boot sector version) 18 | 19 | `"qemu-system-x86_64.exe" -drive format=raw,file=asmle.img` 20 | 21 | ## How does compression work 22 | ### Concept 23 | 24 | The easiest way to compress something is to make it use less bits. We could make each letter a 4 bit index into a charset, cutting the size in half, a word is 2.5 bytes now. But wait, let's not forget we are doing real computing here. Here's our today's guest: padding! Padding turns our sweet 2.5 bytes into 3 bytes, what a rude guy. But don't worry, we gotta make use of extra 0.5 bytes (4 bits) later. 25 | 26 | We need a lookup table for characters. Let's introduce "charset" - **a set of 16 characters**. 27 | We'll have 2 such charsets to make the game more varied and gain more points for added complexity. This is where extra 4 bits go, they become charset index, it takes values from 0 to 1. Padding will help us save space on decompression routine by making it possible to extract all of the values in a sequence. The formula for getting the decompressed character becomes `charset*16 + char`. 28 | 29 | 30 | 31 | Here's the resulting word format 32 | 33 | ``` 34 | cs ch0 ch1 ch2 ch3 ch4 35 | | | | | | | 36 | v v v v v v 37 | 0000 0000 0000 0000 0000 0000 38 | ``` 39 | 40 | ### Generation 41 | 42 | `makedict.py` generates a dictionary by first generating a charset: by picking up 16 random characters from English alphabet and then it takes 32 words that match that set. We do it twice to get 2 dictionaries. 43 | 44 | Then the script writes charsets and the words into a binary file which is then included into the game executable itself. 45 | 46 | Vocabulary format 47 | ``` 48 | charset1 (16 bytes) 49 | charset2 (16 bytes) 50 | 64 words (3 bytes each) 51 | ``` 52 | 53 | ### Decompression 54 | See `main.asm:148` 55 | -------------------------------------------------------------------------------- /asmle.com: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KatPurpy/asmle/71488a981dee61c5d34e8ffbe3f7d7633e67823b/asmle.com -------------------------------------------------------------------------------- /asmle.img: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KatPurpy/asmle/71488a981dee61c5d34e8ffbe3f7d7633e67823b/asmle.img -------------------------------------------------------------------------------- /compile.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set /p BS=Assemble for Boot Sector (bs) or COM (*)?: 3 | set /p RUN=Run? (y/n): 4 | 5 | if %BS% == bs ( 6 | call :COMPILE bs asmle.img 7 | call :RUN dosbox -c "mount C: ." -c "C:" -c "boot asmle.img" 8 | ) else ( 9 | call :COMPILE com asmle.com 10 | call :RUN dosbox -console -debug asmle.com 11 | ) 12 | exit /b 13 | 14 | :COMPILE 15 | if %1 == bs set BOOTSECTOR=-dBOOT_SECTOR 16 | nasm %BOOTSECTOR% -Werror -f bin main.asm -o %2 17 | exit /b 18 | 19 | :RUN 20 | if not %RUN% == y exit /b 21 | if %ERRORLEVEL% == 0 ( 22 | start "Don't be afraid of this window." %* 23 | ) else ( 24 | pause 25 | ) 26 | exit /b -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KatPurpy/asmle/71488a981dee61c5d34e8ffbe3f7d7633e67823b/demo.gif -------------------------------------------------------------------------------- /dict.py: -------------------------------------------------------------------------------- 1 | ADULT 2 | AGENT 3 | ANGER 4 | APPLE 5 | AWARD 6 | BASIS 7 | BEACH 8 | BIRTH 9 | BLOCK 10 | BLOOD 11 | BOARD 12 | BRAIN 13 | BREAD 14 | BREAK 15 | BROWN 16 | BUYER 17 | CAUSE 18 | CHAIN 19 | CHAIR 20 | CHEST 21 | CHIEF 22 | CHILD 23 | CHINA 24 | CLAIM 25 | CLASS 26 | CLOCK 27 | COACH 28 | COAST 29 | COURT 30 | COVER 31 | CREAM 32 | CRIME 33 | CROSS 34 | CROWD 35 | CROWN 36 | CYCLE 37 | DANCE 38 | DEATH 39 | DEPTH 40 | DOUBT 41 | DRAFT 42 | DRAMA 43 | DREAM 44 | DRESS 45 | DRINK 46 | DRIVE 47 | EARTH 48 | ENEMY 49 | ENTRY 50 | ERROR 51 | EVENT 52 | FAITH 53 | FAULT 54 | FIELD 55 | FIGHT 56 | FINAL 57 | FLOOR 58 | FOCUS 59 | FORCE 60 | FRAME 61 | FRANK 62 | FRONT 63 | FRUIT 64 | GLASS 65 | GRANT 66 | GRASS 67 | GREEN 68 | GROUP 69 | GUIDE 70 | HEART 71 | HENRY 72 | HORSE 73 | HOTEL 74 | HOUSE 75 | IMAGE 76 | INDEX 77 | INPUT 78 | ISSUE 79 | JAPAN 80 | JONES 81 | JUDGE 82 | KNIFE 83 | LAURA 84 | LAYER 85 | LEVEL 86 | LEWIS 87 | LIGHT 88 | LIMIT 89 | LUNCH 90 | MAJOR 91 | MARCH 92 | MATCH 93 | METAL 94 | MODEL 95 | MONEY 96 | MONTH 97 | MOTOR 98 | MOUTH 99 | MUSIC 100 | NIGHT 101 | NOISE 102 | NORTH 103 | NOVEL 104 | NURSE 105 | OFFER 106 | ORDER 107 | OTHER 108 | OWNER 109 | PANEL 110 | PAPER 111 | PARTY 112 | PEACE 113 | PETER 114 | PHASE 115 | PHONE 116 | PIECE 117 | PILOT 118 | PITCH 119 | PLACE 120 | PLANE 121 | PLANT 122 | PLATE 123 | POINT 124 | POUND 125 | POWER 126 | PRESS 127 | PRICE 128 | PRIDE 129 | PRIZE 130 | PROOF 131 | QUEEN 132 | RADIO 133 | RANGE 134 | RATIO 135 | REPLY 136 | RIGHT 137 | RIVER 138 | ROUND 139 | ROUTE 140 | RUGBY 141 | SCALE 142 | SCENE 143 | SCOPE 144 | SCORE 145 | SENSE 146 | SHAPE 147 | SHARE 148 | SHEEP 149 | SHEET 150 | SHIFT 151 | SHIRT 152 | SHOCK 153 | SIGHT 154 | SIMON 155 | SKILL 156 | SLEEP 157 | SMILE 158 | SMITH 159 | SMOKE 160 | SOUND 161 | SOUTH 162 | SPACE 163 | SPEED 164 | SPITE 165 | SPORT 166 | SQUAD 167 | STAFF 168 | STAGE 169 | START 170 | STATE 171 | STEAM 172 | STEEL 173 | STOCK 174 | STONE 175 | STORE 176 | STUDY 177 | STUFF 178 | STYLE 179 | SUGAR 180 | TABLE 181 | TASTE 182 | TERRY 183 | THEME 184 | THING 185 | TITLE 186 | TOTAL 187 | TOUCH 188 | TOWER 189 | TRACK 190 | TRADE 191 | TRAIN 192 | TREND 193 | TRIAL 194 | TRUST 195 | TRUTH 196 | UNCLE 197 | UNION 198 | UNITY 199 | VALUE 200 | VIDEO 201 | VISIT 202 | VOICE 203 | WASTE 204 | WATCH 205 | WATER 206 | WHILE 207 | WHITE 208 | WHOLE 209 | WOMAN 210 | WORLD 211 | YOUTH 212 | ADMIT 213 | ADOPT 214 | AGREE 215 | ALLOW 216 | ALTER 217 | APPLY 218 | ARGUE 219 | ARISE 220 | AVOID 221 | BEGIN 222 | BLAME 223 | BREAK 224 | BRING 225 | BUILD 226 | BURST 227 | CARRY 228 | CATCH 229 | CAUSE 230 | CHECK 231 | CLAIM 232 | CLEAN 233 | CLEAR 234 | CLIMB 235 | CLOSE 236 | COUNT 237 | COVER 238 | CROSS 239 | DANCE 240 | DOUBT 241 | DRINK 242 | DRIVE 243 | ENJOY 244 | ENTER 245 | EXIST 246 | FIGHT 247 | FOCUS 248 | FORCE 249 | GUESS 250 | IMPLY 251 | ISSUE 252 | JUDGE 253 | LAUGH 254 | LEARN 255 | LEAVE 256 | LET’S 257 | LIMIT 258 | MARRY 259 | MATCH 260 | OCCUR 261 | OFFER 262 | ORDER 263 | PHONE 264 | PLACE 265 | POINT 266 | PRESS 267 | PROVE 268 | RAISE 269 | REACH 270 | REFER 271 | RELAX 272 | SERVE 273 | SHALL 274 | SHARE 275 | SHIFT 276 | SHOOT 277 | SLEEP 278 | SOLVE 279 | SOUND 280 | SPEAK 281 | SPEND 282 | SPLIT 283 | STAND 284 | START 285 | STATE 286 | STICK 287 | STUDY 288 | TEACH 289 | THANK 290 | THINK 291 | THROW 292 | TOUCH 293 | TRAIN 294 | TREAT 295 | TRUST 296 | VISIT 297 | VOICE 298 | WASTE 299 | WATCH 300 | WORRY 301 | WOULD 302 | WRITE 303 | BINGO -------------------------------------------------------------------------------- /dictionary.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KatPurpy/asmle/71488a981dee61c5d34e8ffbe3f7d7633e67823b/dictionary.bin -------------------------------------------------------------------------------- /main.asm: -------------------------------------------------------------------------------- 1 | ; ASMLE - Wordle in 512 bytes 2 | ; (c) Kat Purpy, 2022 3 | 4 | ; Cheers to Bisqwit and everyone else for help! 5 | 6 | bits 16 7 | %ifdef BOOT_SECTOR 8 | org 0x7c00 9 | %elif 10 | org 100h 11 | %endif 12 | _start: 13 | call rng 14 | call load_word 15 | .loop: 16 | call input_answer 17 | call check_letters 18 | cmp dl,0 19 | jnz .loop 20 | 21 | ;reward player with a nice [CENSORED] 22 | mov al, char_beep 23 | call print_char 24 | 25 | jmp _start 26 | 27 | ; Putting copyright in the unitialized var space because why not 28 | INPUT db 'KATPU' 29 | GAME_WORD db 'RPY,2' 30 | RNG_INDEX db '0' 31 | WORD_ITER_CALLBACK db '22' 32 | 33 | char_backspace equ 0x8 34 | char_carete equ 0xD 35 | char_beep equ 0x7 36 | char_linefeed equ 0xA 37 | 38 | input_answer: 39 | mov dx, 5 40 | .loop: 41 | mov ah, 00h 42 | int 16h ; read char 43 | 44 | xor al, 32 45 | cmp al, char_backspace | 32 46 | je .case_backspace 47 | 48 | call print_char 49 | 50 | mov bx, 5 51 | sub bx, dx 52 | 53 | mov [INPUT+bx], al 54 | 55 | dec dx 56 | cmp dx, 0 57 | jnz .loop 58 | 59 | ret 60 | 61 | .case_backspace: 62 | cmp dx, 5 63 | je .loop 64 | inc dx 65 | mov al, char_backspace 66 | call print_char 67 | 68 | mov bx, 0x0007 69 | mov al, ' ' 70 | mov ah, 09h 71 | mov cx, 1 72 | int 10h 73 | 74 | jmp .loop 75 | 76 | 77 | 78 | ; AL - char to write 79 | print_char: 80 | mov ah, 0Eh 81 | int 10h 82 | ret 83 | 84 | ; output: if DL equals 0 - word is guessed 85 | ; does: 86 | ; - check input 87 | ; - print which letters are correct/wrong/in the word 88 | ; - go to the new line 89 | check_letters: 90 | mov ax, .loop 91 | call iterate_word 92 | 93 | mov al, char_linefeed ; go to the new line 94 | call print_char 95 | ret 96 | 97 | .loop: 98 | mov al, [INPUT+bx] ; input char 99 | mov dh, [GAME_WORD+bx+1] ; word char 100 | cmp al,dh 101 | jnz .letter_wrong 102 | 103 | .letter_correct: 104 | mov bx, 0x000A ; green color 105 | jmp .brk 106 | 107 | .letter_wrong: 108 | inc dl ;add mistake 109 | mov bx, 5 110 | .check: 111 | dec bx 112 | mov dh, [GAME_WORD+bx+1] 113 | cmp al, dh 114 | je .letter_is_in_word 115 | cmp bx, 0 116 | jnz .check 117 | ;letter is not in word 118 | mov bl, 0x0C ; red color 119 | jmp .brk_wrong 120 | 121 | .letter_is_in_word: 122 | mov bl, 0x0E ; yellow color 123 | .brk_wrong: 124 | 125 | .brk: 126 | push ax 127 | mov al, char_backspace ; move the cursor one character back 128 | call print_char 129 | pop ax 130 | 131 | push cx 132 | mov ah, 09h 133 | mov cx, 1 134 | int 10h ; set char color 135 | pop cx 136 | ret 137 | 138 | ; Pardon the name, it just increases RNG_INDEX by one and loops around 139 | rng: 140 | mov al, [RNG_INDEX] 141 | add al, 1 142 | mov si, ax 143 | and si, 63 144 | mov [RNG_INDEX], al 145 | ret 146 | 147 | ; SI - word to decode 148 | load_word: 149 | mov ax, 3 150 | mul si 151 | mov si, ax 152 | add si, 20h 153 | add si, dictionary 154 | 155 | ; Unpack 4 bit values (charset and chars' indices) into GAME_WORD 156 | .unpack: 157 | mov di, GAME_WORD 158 | cld 159 | mov cx, 3 160 | .pair: 161 | lodsb 162 | aam 16 ;magic instruction that extracts 2 nibbles 163 | stosw 164 | loop .pair 165 | 166 | mov ax, .decode 167 | call iterate_word 168 | ret 169 | 170 | ; Get the chars 171 | .decode: 172 | ;dl - char index 173 | mov dl, [GAME_WORD+bx+1] ;get char index 174 | mov bl, [GAME_WORD] ;get dictinary id 175 | shl bl, 4 ;multiply by 16 176 | add bx, dictionary ;add gamedata address 177 | add bl, dl ;add char index 178 | mov dl, [bx] ;store decoded result 179 | mov bx, cx ;store word char number into bx 180 | mov [GAME_WORD+bx+1], dl ;store 181 | ret 182 | 183 | ;AX - pointer to callback function 184 | iterate_word: 185 | mov [WORD_ITER_CALLBACK], ax 186 | mov cx, 5 187 | .iteration: 188 | push cx 189 | dec cx 190 | mov bx, cx 191 | call [WORD_ITER_CALLBACK] 192 | pop cx 193 | loop .iteration 194 | ret 195 | 196 | dictionary equ $ 197 | incbin "dictionary.bin" 198 | times 510-($-$$) db 0 ; Pad remainder of boot sector with 0s 199 | dw 0xAA55 ; The standard PC boot signature -------------------------------------------------------------------------------- /makedict.bat: -------------------------------------------------------------------------------- 1 | python makedicts.py > dictionary.txt 2 | pause -------------------------------------------------------------------------------- /makedicts.py: -------------------------------------------------------------------------------- 1 | import random 2 | from bitarray import bitarray 3 | 4 | dict = open('dict.py','r').read().split('\n') 5 | 6 | def GenerateDict(): 7 | letters = random.sample('ABCDEFGHIJKLMNOPQRSTUVWXYZ',16) 8 | awords = [] 9 | print(letters) 10 | 11 | for w in dict: 12 | add = True 13 | for c in w: 14 | if not c in letters: 15 | add = False 16 | break 17 | if add: awords.append(w) 18 | 19 | words = random.sample(awords,32) 20 | 21 | ewords = [] 22 | for w in words: 23 | indices = [] 24 | for c in w: 25 | indices.append(letters.index(c)) 26 | ewords.append(indices) 27 | print(w,indices) 28 | 29 | return {'DICT': list(map(lambda c: ord(c),letters)),'ENCODED':ewords, 'ASCII': words} 30 | 31 | success = False 32 | a = None 33 | b = None 34 | while not success: 35 | try: 36 | if a is None: a = GenerateDict() 37 | if b is None: b = GenerateDict() 38 | success = True 39 | except: 40 | pass 41 | print(a,b) 42 | 43 | file = open('dictionary.bin','wb') 44 | file.write(bytes(a['DICT'])) 45 | file.write(bytes(b['DICT'])) 46 | 47 | def writewords(words,dictid): 48 | for w in words: 49 | '''w[0] = 0xA 50 | w[1] = 0xB 51 | w[2] = 0xC 52 | w[3] = 0xD 53 | w[4] = 0xE''' 54 | def b(c): return c.to_bytes(1,'little') 55 | 56 | n0 = dictid 57 | n1 = w[0]<<4 58 | n2 = w[1] 59 | n3 = w[2]<<4 60 | n4 = w[3] 61 | n5 = w[4]<<4 62 | 63 | file.write(b(n1|n0)) 64 | file.write(b(n3|n2)) 65 | file.write(b(n5|n4)) 66 | 67 | writewords(a['ENCODED'],0) 68 | writewords(b['ENCODED'],1) 69 | 70 | #print(words[0], words[0].map(lambda c: letters.index(c), letters)) 71 | --------------------------------------------------------------------------------