ASCII animated snow scene

22

3

Write the shortest program to turn any piece of ASCII art into an animated snow scene that begins to form from the falling snow (non-golfed JavaScript example last updated 2011-12-19).

Input specification: Your program must accept arbitrary combinations of spaces, asterisks, and newlines. The input will contain at most 23 lines and 80 characters per line. There will be no empty lines, yet lines may consist of only whitespace. A single trailing newline will be included and must be ignored.

Output: Output ASCII characters (spaces, asterisks) and control codes (carriage returns, linefeeds, ANSI escape codes, etc.) for your operating system's text console or terminal emulator until the user manually terminates the program. You may assume the terminal window is 80x24 characters if your operating system allows that setting.

Rules:

  • The animation must be smooth and fast (15 fps preferred).
  • Snow density must be between 5% and 15%.
  • No more than one screen of snow may scroll per second. (That means no more than 24 lines of new snow may be added in any one second time period.)
  • The snow must not display any obvious pattern as it enters the top of the screen; it must look random.
  • The program must fill all rows of the screen with snow as quickly as possible when it starts; initial filling of the screen's individual rows must not be obvious to the viewer.
  • The lower left corner of the input ASCII art must be at the lower left corner of the screen (Figure 1 for further clarification).
  • The area inside or under the ASCII art must not be permanently filled with asterisks. However, asterisks may (but are not required to) scroll through this area.
  • Snow must not accumulate at the bottom of the screen or on top of existing snow except as shown in the input.
  • Lower spaces must be filled before upper ones, as filling spaces in the opposite order makes the Christmas tree animation look very different from my original code's output. (added 2011-12-20)

Happy holidays!

Figure 1: labeled areas of an 80x24 screen

---------------------------New snow added on this line--------------------------
                                                                             |
                                                                             |
----------------------------------------------------------+                  |
                                                    ****  |                  |
    Snow MUST fall  Snow MAY fall ---------------->  **** |                  |
    through this    through these          ****      **** |  Snow MUST fall  |
    area.           areas of a              ****     **** |  through this    |
                    completed   \--------->  ****     ****|  area.           |
        ASCII art   scene.    \     ***        ****   ****|                  |
          area         \       \   *******      ****  ****|                  |
                        \       \    ********     ***  ***|  (ALL CAPS terms |
      (located in        \       \-->   *********  ***    |  have standard   |
       lower left         \     *******     ******  MAY   |     RFC 2119     |
       corner of           \    *************  **   fall  |    meanings.)    |
       screen)              \        ***********    here  |                  |
                         *** +--->          ****  ***     |                  |
                         *** | ****************   ***     |                  |
  | Snow MUST fall       *** | ****************   ***     |                  |
  | through this         *** +--->                ***     |                  |
  | area.                *** | ****************   ***     |                  |
--+---------------------+*** +--->                ***+----+------------------+--
  |   Snow MUST NOT     |****************************|      Snow MUST NOT    |
  V  accumulate here.   |****************************|     accumulate here.  V

Example inputs

Code Golf Banner

 ******   *******  ********  ********     ******    *******  **       ******** 
**    ** **     ** **     ** **          **    **  **     ** **       **       
**       **     ** **     ** **          **        **     ** **       **       
**       **     ** **     ** ******      **   **** **     ** **       ******   
**       **     ** **     ** **          **    **  **     ** **       **       
**    ** **     ** **     ** **          **    **  **     ** **       **       
 ******   *******  ********  ********     ******    *******  ******** **       

Stack Overflow Logo

                                                    ****
                                                     ****
                                           ****      ****
                                            ****     ****
                                             ****     ****
                                    ***        ****   ****
                                   *******      ****  ****
                                     ********     ***  ***
                                        *********  ***
                                *******     ******
                                *************  **
                                     ***********
                         ***                ****  ***
                         ***   ****************   ***
                         ***   ****************   ***
                         ***                      ***
                         ***   ****************   ***
                         ***                      ***
                         ****************************
                         ****************************

Christmas Trees

                                        *
                                       ***                           *
                *                     *****                         ***
               ***                   *******           *           *****
              *****                 *********         ***            *
                *                  ***********       *****
                       *          *************     *******
        *             ***        ***************       *               *
       ***           *****      *****************                     ***
      *****         *******    *******************                   *****
     *******           *      *********************                 *******
    *********                           *                          *********
        *                                                              *

PleaseStand

Posted 2011-12-12T04:27:51.170

Reputation: 5 369

1The third christmas tree is broken. – Bobby – 2011-12-18T12:07:46.840

Nice challenge! I think the rules should be enumerated for easier reference, and I don't understand the third and sixth rule... – hallvabo – 2011-12-20T13:29:48.443

@hallvabo I have clarified those two rules, the latter by adding a labeled figure. – PleaseStand – 2011-12-20T16:17:45.827

Clarification request: is the newline included in the 80-char maximum line length, or is it max 80 chars plus newline? (I assumed the latter, but some submissions seem to have assumed the former.) – Ilmari Karonen – 2011-12-20T22:30:35.350

@IlmariKaronen The latter. – PleaseStand – 2011-12-21T00:22:36.723

Answers

5

Perl, 196 / 239 chars

chomp(@p=(@f=($"x80)x24,<>)[-24..-1]);{@s=(join("",map rand>.1?$":"*",1..80),@s);if(@s>23){$t=$f[$_],print$_?$/:"\e[H",($f[$_]|=$s[$_]&$p[$_])|($s[$_]&=~$t^$f[$_])for 0..23;select"","","",.1}redo}

This solution differs from your JS example in that the pattern is filled from top down rather than from bottom up, but I assume that's OK since you didn't say anything about it in the rules.

A trivial 1-char reduction can be obtained by replacing \e with a literal ESC character, but that makes the code much harder to read and edit.


Update: I did manage to come up with a version that fills the pattern from bottom up, and doesn't allow snow to fall through the filled parts of the pattern, as in the example JS implementation, at the cost of 43 extra chars:

chomp(@p=(@q=@f=($"x80)x24,<>)[-24..-1]);{@s=(join("",map rand>.1?$":"*",1..80),@s);if(@s>23){my$q;$q[-1-$_]=($q|=$p[-$_]&~$f[-$_])for@a=0..23;print$_?$/:"\e[H",($f[$_]|=$s[$_]&$p[$_]&~$q[$_])|($s[$_]&=~$f[$_])for@a;select"","","",.1}redo}

Replacing ($s[$_]&=~$f[$_]) with just $s[$_] would save 11 chars by letting falling snow pass through the filled parts of the pattern (which matches the spec, but not the example implementation).


OK, since I still seem to be leading the race after a week, I guess I should explain how my solution works to encourage more competition. (Note: This explanation is for the 196-char top-down filling version. I may amend it to include the other version later.)

First of all, the one big trick my solution is based on is that, due to the way ASCII character codes are arranged, the 1-bits in the ASCII code for a space just happen to be a subset of those in the code for an asterisk.

Thus, the following expressions are true: " " & "*" eq " " and " " | "*" eq "*". This is what lets me use bitwise string operations for combining the static and moving parts of the scene without having to loop over individual characters.

So, with that out of the way, let's go over the code. Here's a de-golfed version of it:

chomp(@p = (@f = ($" x 80) x 24, <ARGV>)[-24..-1]);
{
    @s = (join('', map((rand > 0.1 ? $" : '*'), 1..80)), @s);
    if (@s > 23) {
        foreach (0 .. 23) {
            $t = $f[$_];
            print( $_ ? $/ : "\e[H" );
            print( ($f[$_] |= $s[$_] & $p[$_]) | ($s[$_] &= ~$t ^ $f[$_]) );
        }
        select '', '', '', 0.1;
    }
    redo;
}

The first line sets up the arrays @f (for "fixed") and @p (for "pattern"). @f will form the fixed part of the display, and starts out containing nothing but spaces, while @p, which is not shown directly, contains the input pattern; as the animation proceeds, we'll add more and more asterisks to @f until it eventually looks just like @p.

Specifically, @f = ($" x 80) x 23 sets @f to 24 strings of 80 spaces each. ($" is a special Perl variable whose default value just happens to be a space.) We then take this list, append the input lines to it using the readline operator <>, take the last 24 lines of this combined list and assign it to @p: this is a compact way to pad @p with blank lines so that the pattern appears where it should. Finally, we chomp the input lines in @p to remove any trailing newlines so that they won't cause problems later.

Now, let's look at the main loop. It turns out that {...;redo} is a shorter way to write an infinite loop than while(1){...} or even for(;;){...}, especially if we get to omit the semicolon before redo because it immediately follows an if block.

The first line of the main loop introduces the array @s (for "snow", of course), to which it prepends a random 80-character string of 90% spaces and 10% asterisks on every iteration. (To save a few chars, I never actually pop extra lines off the end of the @s array, so it keeps getting longer and longer. Eventually that will grind the program to a halt as the array gets too long to fit in memory, but that will take much longer than most people would ever watch this animation for. Adding a pop@s; statement before the select would fix that at the cost of seven chars.)

The rest of the main loop is wrapped in an if block, so that it only runs once the @s array contains at least 24 lines. This is a simple way to comply with the spec, which requires the whole display to be filled with falling snow from the start, and also simplifies the bitwise operations a bit.

Next comes a foreach loop, which in the golfed version is actually a single statement with a for 0..23 modifier. Since the content of the loop probably needs some explanation, I'm going to unpack it a bit more below:

foreach (0 .. 23) {
    print $_ ? $/ : "\e[H";     # move cursor top left before first line, else print newline
    $t = $f[$_];                # save the previous fixed snowflakes
    $f[$_] |= $s[$_] & $p[$_];  # snowflakes that hit the pattern become fixed 
    $s[$_] &= ~$t ^ $f[$_];     # ...and are removed from the moving part
    print $f[$_] | $s[$_];      # print both moving and fixed snowflakes ORed together
}

First of all, $_ is the default loop counter variable in Perl unless another variable is specified. Here it runs from 0 to 23, i.e. over the 24 lines in the display frame. $foo[$_] denotes the element indexed by $_ in the array @foo.

On the first line of the de-golfed loop, we print either a newline (conveniently obtained from the $/ special variable) or, when $_ equals 0, the string "\e[H", where \e denotes an ESC character. This is an ANSI terminal control code that moves the cursor to the top left corner of the screen. Technically, we could omit that if we assumed a specific screen size, but I kept it in this version since it means I don't have to resize my terminal to run the animation.

On the $t = $f[$_] line, we just save the current value of $f[$_] in a "temporary" variable (hence $t) before potentially changing it in the next line, where $s[$_] & $p[$_] gives the intersection (bitwise AND) of the falling snow and the input pattern, and the |= operator ORs that into the fixed output line $f[$_].

On the line below that, $t ^ $f[$_] gives the bitwise XOR of the previous and current values of $f[$_], i.e. a list of the bits we changed in the previous line, if any, and negating either of the input strings with ~ negates the output. Thus, what we get is a bitmask with all bits set to 1 except those that we just added to $f[$_] on the previous line. ANDing that bitmask onto $s[$_] removes those bits from it; in effect, this means that when a falling snowflake fills in a hole in the fixed pattern, it gets removed from the falling snow array.

Finally, print $f[$_] | $s[$_] (which in the golfed version is implemented by just ORing the two previous lines together) just prints the union (bitwise OR) of the fixed and moving snowflakes on the current line.

One more thing left to explain is the select '', '', '', 0.1 below the inner loop. This is just a klugy way to sleep 0.1 seconds in Perl; for some silly historical reason, the standard Perl sleep command has one-second resolution, and importing a better sleep from the Time::HiRes module takes more chars than abusing 4-arg select.

Ilmari Karonen

Posted 2011-12-12T04:27:51.170

Reputation: 19 513

There seems to be a slight bug: the 24th line is not used, and the bottom line of the ASCII art is the 23rd line. And it really should fill lower spaces before it fills upper ones, although I did not originally specify that. – PleaseStand – 2011-12-21T00:25:21.510

@PleaseStand: I changed the code to use all 24 lines (and incidentally got rid of the say), at the cost of 2 extra chars. I don't think I can change the fill order very easily, though; my implementation is rather fundamentally tied to it. – Ilmari Karonen – 2011-12-21T02:29:04.313

Great explanation! I wish I could up-vote this entry again. – Dillon Cower – 2011-12-21T02:29:32.127

@PleaseStand: I actually did manage to make a bottom-up filling version, which now looks pretty much the same as your JS example; it's a bit longer than the top-down one, but still shorter than the other entries so far. – Ilmari Karonen – 2011-12-21T04:00:06.743

3

HTML and JavaScript, 436 chars

Prepend it to the input:

<body onload="for(a=[],b=[],c=document.body.firstChild,e=c[H='innerHTML'].split(N='\n'),f=e.length-1,g=24,h=g-f;f--;)for(X=80;X--;)b[80*(h+f)+X]='*'==e[f][X];for(setInterval(F='for(y=24;y--;)for(x=80;x--;)if(a[w=80*y+x]){d=1;if(b[w])for(d=0,z=y+1;24>z;++z)b[s=80*z+x]&&!a[s]&&(d=1);d&&(a[w]=0,a[w+80]=1)}for(x=80;x--;).1>Math.random(i=0)&&(a[x]=1);for(t=\'\';1920>i;++i)t+=\'* \'[+!a[i]],79==i%80&&(t+=N);c[H]=t',67);g--;)eval(F)"><pre>

See it run for each example: code golf, Stack Overflow logo, Christmas trees. Internet Explorer users need to run version 9 and set the "Document Mode" to "IE9 standards" (using the F12 developer tools) for this submission to correctly work.

PleaseStand

Posted 2011-12-12T04:27:51.170

Reputation: 5 369

1

All 3 seem to be broken? http://www.pasteall.org/pic/show.php?id=66297

– CodeManX – 2014-02-05T03:50:07.223

1

Python, 299 chars

This should conform to the rules, assuming the newline is included in the 80 char limit.

import random,sys,time
C=1920
v=_,x=' *'
a=['']*C
f=lambda n:[random.choice(e*9+x)for e in _*n]
for e in sys.stdin:a+="%-80s"%e
a=a[-C:]
s=f(C)
while 1:
 z=0;t=''
 for e in s:
    t+=v[x<a[z]or e>_]
    if(e>_<a[z])>(x in a[z+80::80]):a[z]='+'
    t+=z%80/79*'\n';z+=1
 print t;s=f(80)+s[:-80];time.sleep(.1)

hallvabo

Posted 2011-12-12T04:27:51.170

Reputation: 1 640

Your output gets screwy if the input has any lines that are exactly 80 chars (plus newline) long. I've asked PleaseStand to clarify whether that's OK. – Ilmari Karonen – 2011-12-20T22:40:46.540

This is because I include a newline at the end of each 80-char wide line. The description is ambiguous here, since it specifies that newlines must be included, but also that one may assume the terminal is 80 chars wide (in which case one may omit the newlines and depend on automatic wrapping). – hallvabo – 2011-12-20T23:55:39.920

I think the actual problem is that you don't strip off the newlines before padding the input lines to 80 chars, so that an 80-chars-plus-newline input ends up actually appending 81 chars to a and so messes up the indexing. I just tried it, and it looks like replacing %e with %e.rstrip() on line 6 fixes the problem. (Of course, there may well be a shorter fix; I'm no good at Python golf.) – Ilmari Karonen – 2011-12-21T00:10:29.247

If you want to support 81 char lines, just change the numbers, and it works out just fine. As long as you stay under 100 chars per line, it won't change the char count :-) – hallvabo – 2011-12-21T00:14:27.673

If I change your code to use 81-char lines, it won't run correctly on an 80-column terminal, now will it? You're producing 80-column output just fine, it's just that you're not accepting 80-column input correctly. Try it: create an input file with a couple of lines with 80 asterisks on each and see what happens. It shouldn't be that hard: my solution handles it just fine. – Ilmari Karonen – 2011-12-21T02:40:09.730

0

Java, 625 chars

import java.io.*;import java.util.*;class s extends TimerTask {int _c,_k;char _i[],_o[];boolean _b[];public s(String f) throws IOException {_i=new char[23*80];_o=new char[80];_b=new boolean [23*80];BufferedReader br = new BufferedReader(new FileReader(f));while (br.read(_i,_c++*80,80)!=-1);} public void run(){_k=--_k<0?_c:_k;for(int i=0;i<80;_b[_k*80+i]=Math.random()>0.9?true:false,i++);for(int m=0;m<_c;m++){for(int n=0;n<80;_o[n]=_b[(_k+m)%_c*80+n]?'*':_i[m*80+n],n++);System.out.println(_o);}}public static void main(String[] a) throws IOException{Timer timer=new Timer();timer.scheduleAtFixedRate(new s(a[0]),0,500);}}

A simple solution in Java.

SiZ

Posted 2011-12-12T04:27:51.170

Reputation: 21

Nice, but I don't think you're complying with the spec. In particular, you show the whole pattern from the beginning -- it doesn't "form from the falling snow" as in the sample demo. (Admittedly, this could be explained better in the question. You really need to watch the demo to understand what it should do.) Also, your framerate is too slow, you don't start with a snow-filled screen, you seem to be assuming an input format different from the examples given, and you really ought to reset the cursor position between frames (printing "\033[H" should do it). – Ilmari Karonen – 2011-12-16T14:34:59.000