Tips for golfing in QBasic

13

What general tips do you have for golfing in QBasic? I'm looking for ideas that can be applied to code golf problems in general that are at least somewhat specific to QBasic (e.g. "remove comments" is not an answer).

Tips pertaining to the QB64 emulator are also welcome. It has some extra features that aren't in Microsoft QBasic.

DLosc

Posted 2017-11-09T22:31:22.047

Reputation: 21 213

I'm curious about your motivation. I haven't used QBASIC since my 10th grade programming class. Amazing how I saved directly to 1.44 floppy disks without any form of version control and (usually) avoided catastrophic failures. – Andrew Brēza – 2017-11-11T03:21:31.060

5@AndrewBrēza Motivation? The same as my motivation to golf in any language: for fun! I enjoy writing small programs in QBasic (though I wouldn't want to use it for anything serious). There's also the added bonus that it's got sound and graphics (both text and pixel) built in, which my preferred "real" language, Python, does not. – DLosc – 2017-11-11T06:21:28.587

It's much easier to write graphical games in QBasic than in python. – Anush – 2018-06-29T18:46:13.320

If anyone wants to try graphical QBasic applications directly in the browser they could use this: https://github.com/nfriend/origins-host

– mbomb007 – 2018-12-19T15:56:56.280

Answers

10

Know your looping constructs

QBasic has several looping constructs: FOR ... NEXT, WHILE ... WEND, and DO ... LOOP. You can also use GOTO or (in some situations) RUN to loop.

  • FOR ... NEXT is pretty good at what it does. Unlike in Python, it's almost always shorter than the equivalent WHILE or GOTO loop, even when it gets a little fancier:

    FOR i=1TO 19STEP 2:?i:NEXT
    i=1:WHILE i<20:?i:i=i+2:WEND
    i=1:9?i:i=i+2:IF i<20GOTO 9
    

    Note that you don't need to repeat the variable name after NEXT, and you can eliminate space between numbers and most following keywords.

  • WHILE ... WEND is good for when you have a loop that might need to execute 0 times. But if you know the loop will execute at least once, GOTO might be one byte shorter:

    WHILE n>1:n=n\2:WEND
    1n=n\2:IF n>1GOTO 1
    
  • I only use DO ... LOOP for infinite loops (except where RUN can be used instead). While it costs the same number of characters as an unconditional GOTO, it's a little more intuitive to read. (Note that "infinite loop" can include loops that you break out of using a GOTO.) The DO WHILE/DO UNTIL/LOOP WHILE/LOOP UNTIL syntax is too verbose; you're better off using WHILE or GOTO as appropriate.
  • GOTO is, as mentioned above, the shortest general way to write a do/while loop. Use single-digit line numbers instead of labels. Note that when a GOTO is the only thing in the THEN part of an IF statement, there are two equally terse shortcut syntaxes available:

    IF x>y GOTO 1
    IF x>y THEN 1
    

    GOTO can also be used to create more complicated control flows. The naysayers refer to this as "spaghetti code," but this is code golf: unreadability is almost a virtue! GOTO pride!

  • RUN is useful when you need to jump to a fixed place in the program and you don't need to keep any of the variables' values. RUN by itself will restart the program from the top; with a label or line number, it will restart at that line. I've mainly used it to create stateless infinite loops.

DLosc

Posted 2017-11-09T22:31:22.047

Reputation: 21 213

5

Use shortcuts for PRINT and REM

You can use ? instead of PRINT, and ' instead of REM (comment).

' might also come useful when polygloting with languages that support ' as a part of char or string syntax.

Uriel

Posted 2017-11-09T22:31:22.047

Reputation: 11 708

5

Scanner abuse

As in a lot of languages, knowing which characters can and can't be removed is important.

  • Any space next to a symbol can be removed: IF""=a$THEN?0
  • Space can usually be removed between a digit and a letter occurring in that order: FOR i=1TO 10STEP 2. There are some differences between QBasic 1.1 (available at archive.org) and QB64:
    • QBasic 1.1 allows removal of space between any digit and a following letter. Furthermore, in print statements, it will infer a semicolon between consecutive values: ?123x becomes PRINT 123; x. The exceptions to the above are sequences like 1e2 and 1d+3, which are treated as scientific notation and expanded to 100! and 1000# (single- and double-precision, respectively).
    • QB64 is generally the same, but digits can't be followed by d, e, or f at all unless they are part of a well-formed scientific notation literal. (For example, you can't omit the space after the line number in 1 FOR or 9 END, like you can in QBasic proper.) It only infers semicolons in print statements if one of the expressions is a string: ?123"abc" works, but not ?TAB(5)123 or ?123x.
  • Speaking of semicolons, QBasic 1.1 adds a trailing semicolon to a PRINT statement that ends with a call to TAB or SPC. (QB64 does not.)
  • 0 can be omitted before or after the decimal point (.1 or 1.), but not both (.).
  • ENDIF is equivalent to END IF.
  • The closing double quote of a string can be omitted at the end of a line.

DLosc

Posted 2017-11-09T22:31:22.047

Reputation: 21 213

endif actually works in QB64, see this answer – wastl – 2018-06-08T12:15:53.393

@wastl So it does. When I first tested it in QB64, I was using an older version in which it was a syntax error. Thanks for mentioning! – DLosc – 2018-06-08T20:49:42.453

5

Divisibility testing

In programs that require you to test whether one integer is divisible by another, the obvious way is to use MOD:

x MOD 3=0

But a shorter way is to use integer division:

x\3=x/3

That is, x int-div 3 equals x float-div 3.

Note that both of these approaches will return 0 for falsey and -1 for truthy, so you may need to negate the result, or subtract it instead of adding.


If you need the opposite condition (i.e. x is not divisible by 3), the obvious approach is to use the not-equals operator:

x\3<>x/3

But if x is guaranteed to be nonnegative, we can save a byte. Integer division truncates the result, so it will always be less than or equal to float division. Therefore, we can write the condition as:

x\3<x/3

Similarly, if x is guaranteed to be negative, truncation increases the result, and we can write x\3>x/3. If you don't know the sign of x, you'll have to stick to <>.

DLosc

Posted 2017-11-09T22:31:22.047

Reputation: 21 213

4

Know your input methods

QBasic has several ways to get user keyboard input: INPUT, LINE INPUT, INPUT$, and INKEY$.

  • INPUT is your standard multipurpose input statement. The program stops what it's doing, displays a cursor, and lets the user type some input, terminated by Enter. INPUT can read numbers or strings, and it can read multiple values comma-separated. You can specify a string as a prompt, you can go with the default question-mark prompt, and you can even (I just learned this tonight) suppress the prompt altogether. Some sample invocations:
    • INPUT x$,y
      Uses the default ? prompt and reads a string and a number, comma-separated.
    • INPUT"Name";n$
      Prompts with Name? and reads a string.
    • INPUT"x=",x
      Prompts with x= (no question mark! note the comma in the syntax) and reads a number.
    • INPUT;"",s$
      Suppresses the prompt (using the above comma syntax with an empty prompt string), reads a string, and does not move to the next line when the user hits enter (that's what the semicolon after INPUT does). For example, if you PRINT s$ immediately after this, your screen will look like User_inputUser_input.
  • One drawback of INPUT is that you can't read a string with a comma in it, since INPUT uses comma as a field separator. To read a single line of arbitrary (printable ASCII) characters, use LINE INPUT. It's got the same syntax options as INPUT, except it takes exactly one variable which must be a string variable. The other difference is that LINE INPUT does not display a prompt by default; if you want one, you'll have to specify it explicitly.
  • INPUT$(n) displays no prompt or cursor but simply waits until the user enters n characters, and then returns a string containing those characters. Unlike INPUT or LINE INPUT, the user doesn't need to press Enter afterwards, and in fact Enter can be one of the characters (it'll give ASCII character 13, known to C-like languages as \r).

    Most often, this is useful as INPUT$(1), typically in a loop. INPUT$ is good in interactive programs where single keypresses do things. Unfortunately, it only works with keys that have ASCII codes; this includes things like Esc and Backspace, but not the arrow keys, Insert and Delete, and others.

  • Which is where INKEY$ comes in. It is similar to INPUT$(1) in that it returns the results of a single keypress1, but different in that:

    • INKEY$ takes no argument.
    • While INPUT$(n) halts execution until the user enters n characters, INKEY$ does not halt execution. If the user is currently pressing a key, INKEY$ returns a string representing that key; if not, it returns "". This means that if you want to use INKEY$ to get the next keypress, you have to wrap it in a busy-waiting loop:2

      k$=""
      WHILE""=k$
      k$=INKEY$
      WEND
      
    • Both INPUT$ and INKEY$ return ASCII characters for keys that correspond to ASCII characters (including control characters like escape, tab, and backspace). However, INKEY$ can also handle some keys that do not have ASCII codes. For these (saith the help file), "INKEY$ returns a 2-byte string made up of the null character (ASCII 0) and the keyboard scan code."

      Clear as mud? Here's some examples. If you use the INKEY$ loop above to capture a keypress of the left arrow key, k$ will contain the string "␀K" (with the K representing scan code 75). For the right arrow, it's "␀M" (77). Page down is "␀Q" (81). F5 is "␀?" (63).

      Still clear as mud? Yeah. It's not the most intuitive thing in the world. The help file has a table of scan codes, but I always just write a little program to print the results of INKEY$ and press a bunch of keys to find out what the right values are. Once you know which characters correspond to which keys, you can use RIGHT$(k$,1) and LEN(k$) to distinguish between all the different cases you may encounter.

    Bottom line? INKEY$ is weird, but it's the only way to go if your program requires non-blocking input or needs to use the arrow keys.


1 Not including Shift, Ctrl, Alt, PrntScr, Caps Lock, and similar. Those don't count. :^P

2 The WHILE ... WEND idiom here is what I learned in my QBasic books. For golfing purposes, however, a GOTO loop is shorter.

DLosc

Posted 2017-11-09T22:31:22.047

Reputation: 21 213

4

Combine Next Statements

Next:Next:Next

May be condensed down to

Next k,j,i

where the iterators for the For loops are i,j, and k - in that order.

For example the below (69 Bytes)

Input n,m,o
For i=0To n
For j=0To m
For k=0To o
?i;j;k
Next
Next
Next

May be condensed down to 65 Bytes

Input n,m,o
For i=0To n
For j=0To m
For k=0To o
?i;j;k
Next k,j,i

And as far as how this impacts formatting and indentation, I think the best approach to handling this is left aligning the next statement with the outer most for statement. Eg.

Input n,m,o
For i=0To n
    For j=0To m
        For k=0To o
            ?i;j;k
Next k,j,i

Taylor Scott

Posted 2017-11-09T22:31:22.047

Reputation: 6 709

3

In QBasic, it is customary to use the DIM statement to create variables, giving them a name and a type. However, this isn't mandatory, QBasic can also derive a type by the suffix of the variable's name. Since you can't declare and initialise a variable at the same time, it's often wise to skip the DIM in codegolf. Two snippets that are functionally identical*:

DIM a AS STRING: a = "example"
a$ = "example"

* Note that this does create two different variable names.

We can specify the type of the variable by adding $ to the end of a variable name for strings, ! for single precision numbers and % for doubles. Singles are assumed when no type is specified.

a$ = "Definitely a string"
b! = "Error!"

Note that this also holds for arrays. Usually, an array is defined as:

DIM a(20) AS STRING

But arrays also don't need to be DIMmed:

a$(2) = "QBasic 4 FUN!"

a$ is now an array for strings with 11 slots: from index 0 to and including index 10. This is done because QBasic has an option that allows both 0-based and 1-based indexing for arrays. A default array kind of supports both this way.

Remember the twenty-slot array we DIMmed above? That actually has 21 slots, because the same principle applies to both dimmed and non-dimmed arrays.

steenbergh

Posted 2017-11-09T22:31:22.047

Reputation: 7 772

I never realised this applied to arrays too. Interesting. – trichoplax – 2018-06-04T18:23:27.043

3

PRINT (?) has some quirks

Numbers are printed with a leading and trailing space.

Printing adds a linebreak. This behaviour can be altered by adding a comma at the end of the statement to insert a tab instead, or a semi-colon to avoid any insertions:

It is not necessary to use & or ; between distinct operations when printing, eg. ?1"x"s$ shall print the number 1, with spaces on each side, the letter x and the contents of s$

?"foo"
?"bar"
?10
?"foo",
?"bar"
?"foo"; 
?"bar"
?1CHR$(65)
?1" "CHR$(65)
?"A","B

Outputs

foo
bar
 10
foo           bar
foobar
 1 A
 1  A
A             B

Printing a linebreak can be done with just ?

steenbergh

Posted 2017-11-09T22:31:22.047

Reputation: 7 772

Specifically on printing numbers: a space is printed before the number iff it is nonnegative; otherwise, a minus sign - is printed there. A space is also printed after the number. The best way I've discovered to get rid of these spaces is PRINT USING--dunno if you want to add that to this answer or if it should be a separate answer. – DLosc – 2017-11-13T21:50:58.470

3

LOCATE can be really powerful

The LOCATE statement allows you to place the cursor anywhere on the screen (within the usual 80x40 character space limits) and print something at that location. This answer to a challenge really shows this off (and is also combined with a lot of other tips from this topic).

The challenge asks us to output every character a user has pressed in a 16x6 grid. With LOCATE this is simply a matter of div and mod over the ASCII code (a in this code):

LOCATE a\16-1,1+2*(a MOD 16)

And then printing the character:

?CHR$(a)

steenbergh

Posted 2017-11-09T22:31:22.047

Reputation: 7 772

3

Shortening IF statements

IF statements are rather expensive, and golfing them down can save a lot of bytes.

Consider the following (adapted from an answer by Erik the Outgolfer):

IF RND<.5THEN
x=x-1
a(i)=1
ELSE
y=y-1
a(i)=0
ENDIF

The first thing we can do is save the ENDIF by using a single-line IF statement:

IF RND<.5THEN x=x-1:a(i)=1ELSE y=y-1:a(i)=0

This works as long as you don't try to put it on the same line as anything else. In particular, if you have nested IF statements, only the innermost one can be one-lined.

But in this case, we can eliminate the IF entirely using math. Consider what we actually want:

  • If RND<.5 is true (-1), we want:
    • x to decrease by 1
    • y to stay the same
    • a(i) to become 1
  • Otherwise, if RND<.5 is false (0), we want:
    • x to stay the same
    • y to decrease by 1
    • a(i) to become 0

Now if we save the result of the conditional in a variable (r=RND<.5), we can calculate the new values of x, y, and a(i):

  • When r is -1, x=x-1; when r is 0, x=x+0.
  • When r is -1, y=y+0; when r is 0, y=y-1.
  • When r is -1, a(i)=1; when r is 0, a(i)=0.

So our final code looks like:

r=RND<.5
x=x+r
y=y-1-r
a(i)=-r

saving a whopping 20 bytes (40%) over the original version.


The math approach can be applied surprisingly often, but when there's a difference in logic between the two cases (e.g. when you need to input something in one case but not in the other), you will still need to use IF.

DLosc

Posted 2017-11-09T22:31:22.047

Reputation: 21 213

3

Sometimes, you should avoid arrays

Arrays in QBasic, when instantiated without DIM have only 11 slots. If a challenge requires more than 11 slots (or N slots, where N can be bigger than 11), you should DIM the array. Also, let's assume we want to populate this array with data:

DIM a$(12)
a$(0) = "Value 1"
a$(1) = "Value 2"
...

Even golfed, this can take up a lot of space. On such occasions, it might be cheaper in bytes to do this:

a$ = "value 1value 2"

Here, we place everything in 1 concatenated string. Later, we access it like so:

?MID$(a$,i*7,7)

For this approach, it is important that all values are of equal length. Take the longest value and pad out all the others:

a$="one  two  threefour "

You don't need to pad the last value, and you can even skip the closing quotes! If the challenge specifies that white-space is not allowed in the answer, use RTRIM$() to fix that.

You can see this technique in action here.

steenbergh

Posted 2017-11-09T22:31:22.047

Reputation: 7 772

2

WRITE may be useful in place of PRINT

PRINT is usually the way you'll want to do output, since it's pretty flexible and has the ? shortcut. However, the WRITE command can save you bytes in specific situations:

  • When outputting a string, WRITE wraps it in double quotes ("). If you need output with double quotes, WRITE s$ is much shorter than ?CHR$(34);s$;CHR$(34). See, for example, the shortest known QBasic quine.
  • When outputting a number, WRITE does not add spaces before and after it like PRINT does. WRITE n is much shorter than ?MID$(STR$(n),2). See, for example, FizzBuzz in QB64.
  • When outputting multiple values, WRITE separates them with commas: WRITE 123,"abc" outputs 123,"abc". I can't think of a scenario where this would be useful, but that doesn't mean there isn't one.

Limitations of WRITE:

  • There is no way to output multiple values without a separator like with PRINT a;b.
  • There is no way to suppress the newline at the end of output. (You may be able to work around this with LOCATE, but that costs a lot of bytes.)

DLosc

Posted 2017-11-09T22:31:22.047

Reputation: 21 213

1

Sometimes, QBasic mangles inputs to functions. Abuse that!

There's a couple of functions that work on characters instead of strings, but there isn't a char datatype in QBasic, there is only the string ($) type. Take for instance the ASC() function, which returns the ASCII keycode for a character. If we would enter

PRINT ASC("lala")

only the first l would be considered by QBasic. This way, we don't have to bother with cutting a string down to length 1.

Another example comes from this question where the STRING$() function is used in one of the answers.

The STRING$ function takes two arguments, a number n and a string s$, and constructs a string consisting of n copies of the first character of s$

@DLosc, here

Note that QBasic, when offered a multi-char string and needing only one char, automatically takes the first char and ignores the rest.

steenbergh

Posted 2017-11-09T22:31:22.047

Reputation: 7 772