Beefy Boxes and Bandwidth Generously Provided by pair Networks
Welcome to the Monastery
 
PerlMonks  

SOLUTION: Space Invaders

by domm (Chaplain)
on Jun 04, 2002 at 08:01 UTC ( [id://171425]=obfuscated: print w/replies, xml ) Need Help??

This is a de-obfuscation of Space Invaders.

Overview

As this is a rather long text, and the obfuscation isn't an easy one, I'll provide here a short overview on how Space Invaders works, followed by a detailed description.

The game consists of two main parts:

  1. generate the map
  2. move the stuff on the map
Generate the map

The map is in fact one large string (containing line brakes, so it looks two-dimensional when printed). It is generated by the routine j, which gets passed an array consisting of various parts of this string.

Move the stuff on the map

This is done using a set of substitutions on the map-string. E.g. to move a space invader to the right, this regex is used:

$map=~s/p / p/gs;

There are are three different types of substitutions, handled by the routines c, x and z.

Some other features of this obfu

Some other tricks used are:

  • subroutine generation on the fly using eval (in routine a)
  • subroutines that take some values of the argument list (@_) and - after finishing their work - call themself with the rest of the arguments (so I don't have to use while or foreach
  • shifting, pushing and poping the same array (nearly) at the same time
  • fancy quoting
  • the four real subroutines used in the script are named j,a,p,h.
  • The shape of the ASCII-art looks like a PacMan Ghost. So even perl-illiterates get some obfuscation ("hey, this looks like PacMan, but is something completly different ...")
And now, let's look at the guts (Hope you don't get sick easily..)

Adding line breaks:

After adding some line breaks and comments, the code looks like this:

#!/usr/bin/perl use strict; use Term::ReadKey; ReadMode 'cbreak'; # init some stuff $,=$/; $/=++$b+$b; $=='8'; $b=$=x28; $*=$=x2; @!=(qw(4 _ " a 1 1 0 0 )); $![7]=`clear`; # prepare an important string... push(@!,split shift(@!),q 7$=W8W$=8$b eq "j"8W$=8$=W8 $b eq "l"8W$=8W!8($b eq"k"&&$_!~/!/)a#W$=8#$=W8$=W#8W$=#8#!8##a\*8 $=8p$=8$=p8$=q8q$=8p(#.{59})$=8$=$1q8#q(.{60})$=8#$=$1 pap(.{60})!8$![0]++;"\*$1$="8q(.{60})!8$![0]++; "\*$1$="8(t.*)!8$b=(length$1)-61; substr($1,0,$b )."!". substr($1,$b+1).$=4x"gs'"x"c"es'"c"z"gs if' .shift"z"l"just another perl hacker"l4'SpaceInvaders'.($=x12) .'domm@zsi.at j=left k=fire l=right'_'#'x58_("$*p$*"x11). $=x3_$*.("$*$*q"x11).$=_("p$*$*"x11).$=x3_($=x3).("$*$*q"x11)_ 1_$b.$=."W$b"_'#'x 587); # generate board and regex subs j(split shift@!,pop@!); a(split shift@!,pop@!); push(@!,split shift@!,pop@!); $_=$a; map{s/\n//g}@!; map{s/ //g}@!; # start main loop &p; # subs sub END{ ReadMode 'normal' } # init board sub j { $_=shift; $a.=m/^1/?("#$b$=$=$b#$,"x12):"#".eval( )."#$,"; @_&&j(@_) } # generate routines sub a { eval"sub'".pop().'{eval\'s/\'.(shift).\'/\'.(shift).\'/'.pop().';@_& +&'.pop().'(@_)}'; @_&&a(@_) } # main loop sub p{ $b=ReadKey -1; $![1]%3==0&&x(split$=,$![6]); z(split$=,$![4]); x(split$=,$![5]); c(split$=,$![7]); $![1]++; select (@-,@-,@-,0.05); ($![2]=$_)=~s/$=/ /g; /pW|Wq/&&h('Game over'); $_!~/p|q/&&h('You saved our planet. Yeah'); print $![3],$![2]; &p } # print end message sub h { print $![3],shift,"You killed $![0] space invaders",$,; exit }

One trick that's resolved here is the using of ' instead of :: (as in Term'Readkey) or between sub and the subname (as in sub'a).

Lets look at the different sections and explain each one a little bit:

Term::ReadKey

#!/usr/bin/perl use strict; use Term::ReadKey; ReadMode 'cbreak';

Not much obfu here.

Term::ReadKey is used for simple terminal control. Using it in 'cbrake' mode allows me to read in each key as it is pressed, as opposed to the standard perl way of reading from STDIN, where you have to wait for a linebrake to terminate input (not very handy for a game .. )

Init Stuff

# init some stuff $,=$/; $/=++$b+$b; $=='8'; $b=$=x28; $*=$=x2; @!=(qw(4 _ " a 1 1 0 0 )); $![7]=`clear`;

Lets go through this line by line:

$,=$/; sets $, the OUTPUT FIELD SEPARATOR to the value of $/, the INPUT RECORD SEPARATOR. The default of $/ is newline. Setting $, to newline will print a newline between each value passed to print.

$/=++$b+$b; sets $/ to 2. Why 2, I hear you ask. Well, thanks to the dark magic of the pre- and post-autoincrement of $b. Why $b? Because $b (and $a) are the only variable names you don't have declare into a package (using my or the full package name) when running under strict. And why is that? Because $a and $b are used when sorting.

$=='8'; sets $= to 8. $= is only used here so I don't have to declare a varname.

$b=$=x28; sets $b to '88888888888888888888888888888'. As you remember, $= is '8', so the line can be read as $b = '8' x 28. You do know the x operator, do you?

$*=$=x2; sets $* to '88'

$b and $* will be used later to init the game board

@!=(qw(4 _ " a 1 1 0 0 )); loads some values into the @! array. Again a special array name is used to trick strict. The first four values are later shifted and used to split some strings. The last four are used to save the number of space invaders killed, the number of moves, the game board and the clear screan command.

$![7]=`clear`; saves the return value of the clear system call in @!. This speeds up the game a little bit, as only one system call is needed. If we need to clear the screen later on, we only need to print $!7 (or wherever the value will be located then, but see later...)

That was the easey init part...

Prepare the data used for the map and for some code

# prepare an important string... push(@!,split shift(@!), q 7$=W8W$=8$b eq "j"8W$=8$=W8 $b eq "l"8W$=8W!8($b eq"k"&&$_!~/!/)a#W$=8#$=W8$=W#8W$=#8#!8##a\*8 $=8p$=8$=p8$=q8q$=8p(#.{59})$=8$=$1q8#q(.{60})$=8#$=$1 pap(.{60})!8$![0]++;"\*$1$="8q(.{60})!8$![0]++; "\*$1$="8(t.*)!8$b=(length$1)-61; substr($1,0,$b )."!". substr($1,$b+1).$=4x"gs'"x"c"es'"c"z"gs if' .shift"z"l"just another perl hacker"l4'SpaceInvaders'.($=x12) .'domm@zsi.at j=left k=fire l=right'_'#'x58_("$*p$*"x11). $=x3_$*.("$*$*q"x11).$=_("p$*$*"x11).$=x3_($=x3).("$*$*q"x11)_ 1_$b.$=."W$b"_'#'x 587);
OK, this is a little complicated.

push(@!,split shift(@!), obviously pushes something on @!. But what?
This: split shift(@!), something i.e. split something on the first value of @! (and discard this value!). The first value of @! is 4. And the string that gets splited is specified by q 7$=...587);. This uses an obscure feature (some might say bug) of q quoting: If you put a space after the q you can use any character as a quote. In this case, 7.

So, the code can be written clearer like this:

push(@! split(4,"$=W...58"));

OK, but what gets pushed on @!? If you split the quoted string on 4's, you get this array:

$=W8W$=8$b eq "j"8W$=8$=W8 $b eq"l"8W$=8W!8($b eq"k"&&$_!~/!/)a#W$=8#$ +=W8$=W#8W$=#8#!8##a\*8$=8p$=8$=p8$=q8q$=8p(#.{59})$=8$=$1q8#q(.{60})$ +=8#$=$1pap(.{60})!8$![0]++;"\*$1$="8q(.{60})!8$![0]++;"\*$1$="8(t.*)! +8$b=(length$1)-61;substr($1,0,$b )."!". substr($1,$b+1).$= <br> x"gs'"x"c"es'"c"z"gs if'.shift"z"l"just another perl hacker"l <br> 'SpaceInvaders'.($=x12).'domm@zsi.at j=left k=fire l=right'_'#'x58_("$ +*p$*"x11). $=x3_$*.("$*$*q"x11).$=_("p$*$*"x11).$=x3_($=x3).("$*$*q" +x11)_1_$b.$=."W$b"_'#'x 58
Still not that clear, but that's all we can do now. So, we'll continue..

Start doing something..

# generate board and regex subs j(split shift@!,pop@!); a(split shift@!,pop@!); push(@!,split shift@!,pop@!); $_=$a; map{s/\n//g}@!; map{s/ //g}@!; # start main loop &p;

Subroutine j - Init the Map

j(split shift@!,pop@!); calls the subroutine j. You might recognise the split shift pop from above. Here we split the last element of @! (the block starting with 'SpaceInvaders'.. using the first element of @! (_, discarding both.

That is, the following array gets passed to j:

( 'SpaceInvaders'.($=x12).'domm@zsi.at j=left k=fire l=right', '#'x58, ("$*p$*"x11).$=x3_$*.("$*$*q"x11).$=, ("p$*$*"x11).$=x3_($=x3).("$*$*q"x11), 1, $b.$=."W$b", '#'x 58, )
Or, a little bit more readable (I substituded some vars with their value):

( 'SpaceInvaders'.('8'x12).'domm@zsi.at j=left k=fire l=right', '#'x58, ("88p88"x11).'8'x3, '88'.("8888q"x11).'8', ("p8888"x11).'8'x3 ('8'x3).("8888q"x11), 1, '88888888888888888888888888888'.'8'."W88888888888888888888888888888", '#'x 58, )
If you replace the '8' with space, you can get a first glimpse of the map.

Now, the sub:

# init map sub j { $_=shift; $a.=m/^1/?("#$b$=$=$b#$,"x12):"#".eval( )."#$,"; @_&&j(@_) }
After you know the data getting passed to it, it is more or less clear what happens:

The first value of the array gets assigned to $_.
Something is appended to $a (again, $a gets ignored by strict). If $_ starts with '1', this string gets appended twelve times:
#888888888888888888888888888888888888888888888888888888888888#
As you might guess, this is are the "blank lines" between the space invaders and the cannon. Note the $, at the end, which contains the line break
If the value doesn't start with '1', then append '#', the evaled value, another '#' and a line break ($,). The evaled value will be either the first line, the top border, a sequence of space invaders (p or q), the cannon (W) or the bottom border.

The last line, @_&&j(@_) calls j again if there are still values in the array. If all values have been shifted off the array, we're finshed with the map and return to our regularly scheduled program.

Subroutine a - Make some subs

a(split shift@!,pop@!);

You should know this by now. Split the last element of @! using the first, i.e. split this thing
x"gs'"x"c"es'"c"z"gs if'.shift"z"l"just another perl hacker"l
using ", which yields this array:

(x, gs', x, c, es', c, z, gs if'.shift, z, l , just another perl hacke +r ,l)
(Note that this should be quoted)

Now, let's look at a

sub a { eval"sub'".pop().'{eval\'s/\'.(shift).\'/\'.(shift).\'/'.pop().';@_& +&'.pop().'(@_)}'; @_&&a(@_) }

Here we generate a sub using eval. But it's kind of hard to see how those subs will look like in all this shift-pop-slashing. I'll show you one finished sub with the poped params in red:

sub x { eval 's/'.(shift).'/'.(shift).'/gs';@_&&x(@_) }

Some things to note here:

  • The subs are generated in reverse order, as the values of @_ are poped off.
  • l is never used. I just had some space left, and filled with some random string :-)
  • The three generated subs use the same @_&&sub(@_) trick employed to generate them.
  • The subs will be used later to move the stuff around on the map.
  • That's what I love about perl: If you have similar-looking code, don't write it yourself, but let perl do it. Very lazy.

Add some more data

push(@!,split shift@!,pop@!); Once again the push-split-pop thing, this time a even more wierd, as we push onto the same array that we shift and pop on. So, this gets pushed onto @!:

( $=W8W$=8$b eq "j"8W$=8$=W8 $b eq"l"8W$=8W!8($b eq"k"&&$_!~/!/) #W$=8#$=W8$=W#8W$=#8#!8## \*8$=8p$=8$=p8$=q8q$=8p(#.{59})$=8$=$1q8#q(.{60})$=8#$=$1p p(.{60})!8$![0]++;"\*$1$="8q(.{60})!8$![0]++;"\*$1$="8(t.*)!8$b=(lengt +h$1)-61;substr($1,0,$b )."!". substr($1,$b+1).$= )
Still looks like line noise.

Prepare some stuff

$_=$a; map{s/\n//g}@!; map{s/ //g}@!;
Assign $a (the map, you remember...) to $_, so that the substitutions will work
remove line break and two spaces from @!. This removes the eyes of the Pacman-Ghost-Form and the line break from the source codes. Ah, those are maps in void context. Don't do this. (Once we have perl6, we'll be able to alter all elements of an array using the cool hyper-operator. This will allow for interesting new obfus, I'm sure...).

Subroutine p - Let the game begin

sub p{ $b=ReadKey -1; $![1]%3==0&&x(split$=,$![6]); z(split$=,$![4]); x(split$=,$![5]); c(split$=,$![7]); $![1]++; select (@-,@-,@-,0.05); ($![2]=$_)=~s/$=/ /g; /pW|Wq/&&h('Game over'); $_!~/p|q/&&h('You saved our planet. Yeah'); print $![3],$![2]; &p }

OK, after all this boring initialising and code generating, let's grill some aliens ..

$b=ReadKey -1; gets the pressed key and stores in $b. $b used to hold a lot of '8's, but the map is finsihed now, so we can overwrite it.

$![1]%3==0&&x(split$=,$![6]);

If the modulus of

$![1]</code< (this is where we store the number of moves) and 3 is zer +o (i.e. every three moves), call <i>x</i>. <i>x</i> is one of the aut +ogenerated subs that does some substitution on $_. It substitutes the + first value passed to it with the second, until there are no values +left.<br> What gets passed to <i>x</i> is <code>$![6]
splited on $= ('8'), i.e.:
\* $= p$= $=p $=q q$= p(#.{59})$= $=$1q q(.{60})$= #$=$1p
(formatted in pairs).
So, "\*" gets replaced by "8", "p8" by "8p", etc. The last two are more complex:
/p(#.{59})$=/$=$1q/ replace "p" followed by (start capturing parenthesis) "#" and 59 other chars, followed by '8' with '8', the captures stuff, and 'q'. As you might have guessed, this happens when a space invaders reaches the right border and moves on to next line, changing his direction (i.e. is now a 'q')

So, this line moves the space invaders every three moves. If the game is to easy or hard, you can change the %3 to %2 or %4 for faster or slower space invaders.

z(split$=,$![4]); x(split$=,$![5]); c(split$=,$![7]);
Those work more or less the same as described above:

z moves the cannon and fires it. Note that z takes three arguments, two regexes and one condition. The cannon only moves if you pressed the right key.

x again just moves some things. In this case it prevents the cannon from reaching the left and rigth border and removes the cannon shot from the top border. This can happen every move, not just ever third.

c moves the cannon shot. In c, the substitution uses the emodifier, thus evaling the second part of the substitution. Because of this we can count the hits here. $![1]++; counts the moves.

select (@-,@-,@-,0.05); sleeps for 0.05 seconds.

($![2]=$_)=~s/$=/ /g; change all the '8' to spaces and stores the map in $!2

/pW|Wq/&&h('Game over');. If a space invader reaches the cannon, you've lost. Welcome to our new alien overlords.

$_!~/p|q/&&h('You saved our planet. Yeah');. If no invaders are left, you did it.

print $![3],$![2];. This prints a clear screan and the map

&p. Repeat.

Boring leftovers

sub h { print $![3],shift,"You killed $![0] space invaders",$,; exit }

This prints the end message and the number of invaders killed.

sub END{ ReadMode 'normal' }

Sets the ReadMode back to normal. Without this, your terminal will behave rather strange after the game. This is registerd as an END block, so it gets calles even if you press CTRL-C.

Hope you enjoyed this as much as I did ...

--
#!/usr/bin/perl -w just another perl hacker
print+seek(DATA,$=*.3,@-)?~~<DATA>:$:__DATA__

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: obfuscated [id://171425]
Approved by jmcnamara
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others musing on the Monastery: (8)
As of 2024-03-28 09:58 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found