Stack OverflowCode Golf: Playing Tetris
[+83] [14] ChristopheD
[2010-10-04 19:18:26]
[ language-agnostic code-golf rosetta-stone ]
[ ]

The basics:

Consider the following tetrominoes and empty playing field:

    I   O    Z    T    L    S    J         [          ]
                                           [          ]
    #   ##   ##   ###  #     ##   #        [          ]
    #   ##    ##   #   #    ##    #        [          ]
    #                  ##        ##        [          ]
    #                                      [          ]

The dimensions of the playing field are fixed. The numbers at the top are just here to indicate the column number (also see input).


1. You are given a specific playing field (based on the above) which can already be filled partly with tetrominoes (this can be in a separate file or provided via stdin).

Sample input:

[          ]
[          ]
[          ]
[          ]
[ #    #  #]
[ ## ######]

2. You are given a string which describes (separated by spaces) which tetromino to insert (and drop down) at which column. Tetrominoes don't need to be rotated. Input can be read from stdin.

Sample input:

T2 Z6 I0 T7

You can assume input is 'well-formed' (or produce undefined behaviour when it's not).


Render the resulting field ('full' lines must disappear) and print the score count (every dropped line accounts for 10 points).

Sample output based on the sample input above:

[          ]
[          ]
[          ]
[#      ###]
[#     ### ]
[##### ####]


Shortest solution (by code character count). Usage examples are nice. Have fun golfing!

Edit: added a bounty of +500 reputation to draw some more attention to the nice efforts the answerers already made (and possibly some new solutions to this question)...

(2) No J tetromino? - ninjalj
@ninjalj: good point, added the J tetromino ;-) - ChristopheD
Question. How's input counted toward total? I'm going to try this in Mathematica, which doesn't do "stdin". :) - Gleno
@Gleno: in this case (when the language doesn't support stdin and/or file reading), the bare input shouldn't count towards the total. - ChristopheD
We can file read just fine, but I'm assuming the path length doesn't count? I've been scratching my head for the last 15 minutes, this will be a hard one. :) - Gleno
(2) Well if you can use relative paths (current directory for example) I think path should count (~the same problem~ in other languages ;-) Kudos for trying to solve this in Mathematica! - ChristopheD
Can 2nd part of input be after 1st? I mean, T2 ... and then the start position. - Nakilon
@Nakilon: yes, that wouldn't be a problem. - ChristopheD
(1) Flagged. This isn't a competition site, this is a site to help get answers to REAL questions, not to some puzzle. - Rudolf Olah
(5) @omouse: check - code golf is generally allowed (in community wiki form) - ChristopheD
(18) @omouse: That's what voting to close is for. Dragging moderators here by flagging the question probably isn't going to make you that popular, given that time and time again the community has (reluctantly) allowed code golf to exist (see the code-golf tag and meta discussions; it's nothing new). - Mark Peters
(8) @omouse: Off-topic != spam. Even if you can't vote to close, that spam flag was uncalled for. - BoltClock
a bit unfair to award the overall shortest and not per language, no? this way my preferred python has no chance against perl or J. btw how is award distributed if there are multiple contributors in an entry? - Nas Banov
@Nas Banov, Python sometimes wins. Also, previous golf (ascii-boxes) won by JavaScript. It's normal for codegolf to be a contest between different usual popular practical languages. - Nakilon
@Nakilon: it would be more exact to say that Javascript tied for the first place. - ninjalj
(1) 500 Bounty is enough to justify a BF implementation. Someone make one! (Not it!) - Brian
(3) I am waiting for an APL jock! I'll bet he can do it in 3.5 symbols - n8wrl
(1) Even though I like CG, I think it should be done without a rep gain as then rep gain is not justified by helping someone else to solve their problem in the spirit of the site; the bounty is effectively a massive rep gain. - Callum Rogers
@Nakion: have a hard look at before making statement how fair it is to compare apples and oranges. The best golfers in Perl, Python, Ruby and PHP had fine tuned their solutions there and you can see clear tendencies of which-is-shorter. PS. after running the numbers on the 24 challenges there, i can say that on the Perl and Ruby on average are the shortest, with Python solutions being ~40% longer and PHP ones being ~60% longer. - Nas Banov
(3) The dimensions are supposed to be fixed, but the sample input and blank field have different heights. What's the height supposed to be? - Nabb
@Nas Banov: I can't influence the fact that the bounty is to be awarded to a single answer (and in code-golf this should definitely be the shortest working solution). - ChristopheD
@Callum Rogers: the question (probably posting at a wrong time of day) got buried (with only 2 answers and low views) quite quickly. I thought that putting a bounty on it was a good way to draw attention to the question (and to see some more competition). I might have overdone it a little with the +500 bonus though ;-) - ChristopheD
@Nabb: Good catch! The sample input is the right size (height = 7) - ChristopheD
@ChristopheD: yes i know you dont get freedom in that. at least the big award was enough to bring attention and people to the q. as intended. Could you list more examples that code should pass before being considered a winner? e.g. a partly filled board w/0 tetrominoes (needs only to remove full lines before output); or "completely" filled board that should be collapsed before first tetromino is dropped etc - Nas Banov
@Nas Banov: thanks for commenting! The bounty indeed drew attention (and answers). I honestly feel that the examples you gave (e.g. the need for processing full lines before first input) would be changing the spec (and it would be unfair - and far too late to do that at this time). - ChristopheD
@ChristopheD: are you saying that the ONLY requirement for the implementation is to be able to process the sample input "T2 Z6 I0 T7"? That can be done with a single "print"! Perhaps you are right i gave boundary cases - even if i dont see them "changing" anything from the spec - but where do you draw the line? You have to have more than 1 example to test agaings. The reason being, if you have incomplete specification (which is the case, we seem to agree), some stuff is clarified by test samples itself... at least that's my experince from playing - Nas Banov
@Nas Banov: I think the spec is clear (with a single example which 'tests' some various things and the written specifications. I feel the 'border cases' you described would be 'adding to the spec' (and thus should not be assumed in answering the question). But I'll be sure to include more test cases if I post another code golf question! - ChristopheD
Zipping is not golfing. - Nakilon
(1) @Nas Banov: It's a bit of poor sportsmanship to deride the work of the other programmers, some of whom very clearly put in a lot of effort and learning, as evidenced by their detailed explanations. I note your entry lacks any explication, which, although good for your ego, reduces its value to the casual observer well below that of the "weaker" entries you mention. - Conspicuous Compiler
@Conspicuous Compiler: You seem new around golfing (there is not a single tag code-golf in your profile). You shant be swift dispatching justice in areas you are a "casual observer". What makes you think i "derided" others work? What i said is that I am puzzled that usually solutions are notably shorter. For each <problem, language> pair there is a kind of limit inferior to which solutions tend to converge, eventually. In regards to explanations, pls just ask what is unclear (on condition that you know the language already, since "those margins are too narrow to contain" a full tutorial). - Nas Banov
(1) @Nas Banov: Nice of you to respond to my statement by starting with an ad hominem attack. Calling other people's code "weak" is bad style, plain and simple. Please don't be snarky just because I'm correcting you for poor behavior. - Conspicuous Compiler
[+27] [2010-10-08 07:53:04] Nabb [ACCEPTED]

GolfScript - 181 characters

Newlines are not necessary. Output is in standard output, although some errors are present in stderr.
\10 should be replaced by the corresponding ASCII character for the program to be 181 characters.

{):X!-{2B{" #"=}%X" ":f*+-1%}%:P;:>.{\!:F;>P{\(@{3&\(@.2$&F|:F;|}%\+}%\+F![f]P+:P
;}do;{"= "&},.,7^.R+:R;[>0="#"/f*]*\+}0"R@1(XBc_""~\10"{base}:B/3/~4*"nIOZTLSJR "
";:"*~;n%)n*~ 10R*+n*

Sample I/O:

$ cat inp
[          ]
[          ]
[          ]
[          ]
[ #    #  #]
[ ## ######]
T2 Z6 I0 T7
$ cat inp|golfscript 2>/dev/null
[          ]
[          ]
[          ]
[#      ###]
[#     ### ]
[##### ####]

Tetromino compression:
Pieces are stored as three base 8 digits. This is a simple binary representation, e.g.T=[7,2,0], S=[6,3,0], J=[2,2,3]. [1] is used for the I piece in compression, but this is explicitly set to [1,1,1,1] later (i.e. the 4* in the code). All of these arrays are concatenated into a single array, which is converted into an integer, and then a string (base 126 to minimize non-printable characters, length, and not encounter utf8). This string is very short: "R@1(XBc_".

Decompression is then straightforward. We first do a base 126 conversion followed by a base 8 conversion ("~\10"{base}/, i.e. iterate through "~\10" and do a base conversion for each element). The resulting array is split into groups of 3, the array for I is fixed (3/~4*). We then convert each element to base 2 and (after removing zeros) replace each binary digit with the character of that index in the string " #" (2base{" #"=}%...-1% - note that we need to reverse the array otherwise 2 would become "# " instead of " #").

Board/piece format, dropping pieces
The board is simply an array of strings, one for each line. No work is initially done on this, so we can generate it with n/( on the input. Pieces are also arrays of strings, padded with spaces to the left for their X position, but without trailing spaces. Pieces are dropped by prepending to the array, and continuously testing whether there is a collision.

Collision testing is done by iterating through all characters in the piece, and comparing against the character of the same position on the board. We want to regard #+= and #+# as collisions, so we test whether ((piecechar&3)&boardchar) is nonzero. While doing this iteration, we also update (a copy of) the board with ((piecechar&3)|boardchar), which correctly sets the value for pairs #+, +#, +[. We use this updated board if there is a collision after moving the piece down another row.

Removing filled rows is quite simple. We remove all rows for which "= "& return false. A filled row will have neither = or , so the conjunction will be a blank string, which equates to false. Then we count the number of rows that have been removed, add the count to the score and prepend that many "[ ... ]"s. We generate this compactly by taking the first row of the grid and replacing # with .

Since we compute what the board would look like in each position of the piece as it falls, we can keep these on the stack instead of deleting them! For a total of three characters more, we can output all these positions (or two characters if we have the board states single spaced).

{):X!-{2B{" #"=}%X" ":f*+-1%}%:P;:>.{>[f]P+:P(!:F;{\(@{3&\(@.2$&F|:F;|}%\+}%\+F!}
do;{"= "&},.,7^.R+:R;[>0="#"/f*]*\+}0"R@1(XBc_""~\10"{base}:B/3/~4*"nIOZTLSJR "
";:"*~;n%)n*~ ]{n*n.}/10R*

Some extreme code-golfing going on right here (I didn't think this could be done in less then 200 characters). Nice job! - ChristopheD
(8) Amazing. I wish I understood GolfScript. Wait ... no I don't. - P Daddy
[+26] [2010-10-04 21:47:03] ninjalj

Perl, 586 523 483 472 427 407 404 386 387 356 353 chars

(Needs Perl 5.10 for the defined-or // operator).

Takes all input from stdin. Still needs some serious golfing.
Note that ^Q represents ASCII 17 (DC1/XON), ^C represents ASCII 3 and ^@ represents ASCII 0 (NUL).

while(<>){push@A,[split//]if/]/;while(/\w/g){for$i(0..6){for($f=0,$j=4;$j--;){$c=0;map{if($_){$i--,$f=$j=3,redo if$A[$k=$i+$j][$C=$c+$'+1]ne$";$A[$k][$C]="#"if$f}$c++}split//,unpack"b*",chr vec"3^@'^@c^@^Q^C6^@\"^C^Q^Q",index(OTZLSJI,$&)*4+$j,4;$s+=10,@A[0..$k]=@A[$k,0..$k-1],map{s/#/ /}@{$A[0]},$i++if 9<grep/#/,@{$A[$k]}}last if$f}}}print+(map@$_,@A),$s//0,$/

Commented version:

    # store the playfield as an AoA of chars
    # while we're getting pieces
            # for each line of playfield
                    # for each line of current piece
                            # for each column of current piece
                                            # if there's a collision, restart loop over piece lines
                                            # with a mark set and playfield line decremented
                                            $i--,$f=$j=3,redo if$A[$k=$i+$j][$C=$c+$'+1]ne$";
                                            # if we already found a collision, draw piece
                            # pieces are stored as a bit vector, 16 bits (4x4) per piece,
                            # expand into array of 1's and 0's
                            }split//,unpack"b*",chr vec"3^@'^@c^@^Q^C6^@\"^C^Q^Q",index(OTZLSJI,$&)*4+$j,4;
                            # if this playfield line is full, remove it. Done by array slicing
                            # and substituting all "#"'s in line 0 with " "'s
                            $s+=10,@A[0..$k]=@A[$k,0..$k-1],map{s/#/ /}@{$A[0]},$i++if 9<grep/#/,@{$A[$k]}
                    # if we found a collision, stop iterating over the playfield and get next piece from input
                    last if$f
# print everything

Edit 1: some serious golfing, fix output bug.
Edit 2: some inlining, merged two loops into one for a net saving of (drum roll...) 3 chars, misc golfing.
Edit 3: some common subexpression elimination, a little constant merging and tweaked a regex.
Edit 4: changed representation of tetrominoes into a packed bit vector, misc golfing.
Edit 5: more direct translation from tetromino letter to array index, use non-printable characters, misc golfing.
Edit 6: fixed bug cleaning top line, introduced in r3 (edit 2), spotted by Nakilon. Use more non-printable chars.
Edit 7: use vec for getting at tetromino data. Take advantage of the fact that the playfield has fixed dimensions. if statement => if modifier, the merging of loops of edit 2 starts paying off. Use // for the 0-score case.
Edit 8: fixed another bug, introduced in r6 (edit 5), spotted by Nakilon.
Edit 9: don't create new references when clearing lines, just move references around via array slicing. Merge two map's into one. Smarter regex. "Smarter" for. Misc golfings.
Edit 10: inlined tetromino array, added commented version.

Working very nicely (and already at a nice character count for this non-trivial problem). One small peculiarity is that my perl (perl, v5.10.0 built for darwin-thread-multi-2level) seems to print the result twice (input piped in). - ChristopheD
@ChristopheD: fixed the duplicated output, I was printing inside my main loop, but only for lines without playfield. You probably had a newline too much :) - ninjalj
4 more chars to beat python!! - Vivin Paliath
(1) I haven't given up yet perl! xD (Although I would like to see some other solutions too by now..) - poke
@Nakilon: Good catch! You have a nice test case there. - ninjalj
@ninjalj, but 1st test is from task. And now (404chars) raises error on both tests. I tested on some fresh Strawberry Perl I installed yesterday. - Nakilon
@Nakilon: note that now I'm using control characters, so you will need to input them manually on your text editor. - ninjalj
@Nakilon: use a real text editor ;P In vim, Ctrl-V + 3-digit ASCII code (probably Ctrl-Q on Windows, due to Ctrl-V being already mapped). - ninjalj
No IDE holywars! ) - Nakilon
If you didn't changed tetrominos' data, which I copypasted from 404chars, then you program fails on this test: - Nakilon
Do you ever sleep? ) - Nakilon
Did you do the conversion from commented to mini by hand or do you have a program to do that for you? Please answer here: - Bryan Field
@cjavapro: Actually, I did the conversion from mini to commented. All those comments and newlines and indents are just distracting when code-golfing! - ninjalj
[+24] [2010-10-06 04:21:28] Nakilon

Ruby — 427 408 398 369 359

a=u[t.reverse.join.scan /#{'( |#)'*10}/]{|w|m=(g='I4O22Z0121T01201L31S1201J13'[/#{w[0]}\d+/].scan(/0?\d/).zip a.drop w[1].to_i).map{|r,b|(b.rindex ?#or-1)-r.size+1}.max{|r,b|b.fill ?#,m+r.size,r.to_i}
v.reject!{|i|i-[?#]==[]&&(o+=10;v)<<[' ']*10}
puts u[a]{|i|?[+i*''+?]},t[-1],o

Very nice solution! I'll have to take a look at how exactly you encoded the terominoes forms (looks very compact this way). - ChristopheD
(3) I'd really love to see an expanded explanation of this code. It looks so sick… can't get my head around it. - Nils Riedemann
(1) @Nils Riedemann, I write an explanation right now, but am thinking of to post it now, or after winner announce ) Anyway once I'll post and answer all questions, because it's a community wiki with main idea to be useful ) - Nakilon
On Debian's ruby 1.9.2dev (2010-07-30) this fails for your test case at .Also, it always extends the playfield to ten lines? - ninjalj
@ninjalj, ruby 1.9.2p0 (2010-08-18) [i386-mingw32] Looks fine. Width of 10 is a Tetris standart, I suppose. - Nakilon
My input file had a newline too many. As for the 10 lines, I meant lines, not columns, when I try the original example, the output playfield is 10 lines high. - ninjalj
Ventero is a winner. - Nakilon
[+17] [2010-10-11 01:23:12] PleaseStand

Bash shell script (301 304 characters)

UPDATE: Fixed a bug involving pieces that extend into the top row. Also, the output is now sent to standard out, and as a bonus, it is possible to run the script again to continue playing a game (in which case you must add up the total score yourself).

This includes nonprintable characters, so I have provided a hex dump. Save it as tetris.txt:

0000000: 7461 696c 202d 3120 245f 7c7a 6361 743e  tail -1 $_|zcat>
0000010: 753b 2e20 750a 1f8b 0800 35b0 b34c 0203  u;. u.....5..L..
0000020: 5590 516b 8330 10c7 dff3 296e 4c88 ae64  U.Qk.0....)nL..d
0000030: a863 0c4a f57d 63b0 07f7 b452 88d1 b4da  .c.J.}c....R....
0000040: 1a5d 5369 91a6 df7d 899a d05d 5e72 bfbb  .]Si...}...]^r..
0000050: fbff 2fe1 45d5 0196 7cff 6cce f272 7c10  ../.E...|.l..r|.
0000060: 387d 477c c4b1 e695 855f 77d0 b29f 99bd  8}G|....._w.....
0000070: 98c6 c8d2 ef99 8eaa b1a5 9f33 6d8c 40ec  ...........3m.@.
0000080: 6433 8bc7 eeca b57f a06d 27a1 4765 07e6  d3.......m'.Ge..
0000090: 3240 dd02 3df1 2344 f04a 0d1d c748 0bde  2@..=.#D.J...H..
00000a0: 75b8 ed0f 9eef 7bd7 7e19 dd16 5110 34aa  u.....{.~...Q.4.
00000b0: c87b 2060 48a8 993a d7c0 d210 ed24 ff85  .{ `H..:.....$..
00000c0: c405 8834 548a 499e 1fd0 1a68 2f81 1425  ...4T.I....h/..%
00000d0: e047 bc62 ea52 e884 42f2 0f0b 8b37 764c  .G.b.R..B....7vL
00000e0: 17f9 544a 5bbd 54cb 9171 6e53 3679 91b3  ..TJ[.T..qnS6y..
00000f0: 2eba c07a 0981 f4a6 d922 89c2 279f 1ab5  ...z....."..'...
0000100: 0656 c028 7177 4183 2040 033f 015e 838b  .V.(qwA. @.?.^..
0000110: 0d56 15cf 4b20 6ff3 d384 eaf3 bad1 b9b6  .V..K o.........
0000120: 72be 6cfa 4b2f fb03 45fc cd51 d601 0000  r.l.K/..E..Q....

Then, at the bash command prompt, preferably with elvis rather than vim installed as vi:

$ xxd -r tetris.txt
$ chmod +x
$ cat << EOF > b
> [          ]
> [          ]
> [          ]
> [          ]
> [ #    #  #]
> [ ## ######]
> [==========]
$ ./ T2 Z6 I0 T7 2>/dev/null
-- removed stuff that is not in standard out --
[          ]
[          ]
[          ]
[#      ###]
[#     ### ]
[##### ####]

How it works

The code self-extracts itself similarly to how executable programs compressed using the gzexe script do. Tetromino pieces are represented as sequences of vi editor commands. Character counting is used to detect collisions, and line counting is used to calculate the score.

The unzipped code:

echo 'rej.j.j.:wq!m'>I
echo '2rejh.:wq!m'>O
echo '2rej.:wq!m'>Z
echo '3rejh1.:wq!m'>T
echo 'rej.j2.:wq!m'>L
echo 'l2rej2h.:wq!m'>S
echo 'lrej.jh2.:wq!m'>J
for t
do for y in `seq 1 5`
do echo -n ${y}jk$((${t:1}+1))l|cat - ${t:0:1}|vi b>0
grep ========== m>0||break
[ `tr -cd '#'<b|wc -c` = `tr -cd '#'<m|wc -c` ]||break
tr e '#'<m>n
cat n>b
grep -v '##########' b>m
$((S+=10*(`wc -l < b`-`wc -l < m`)))
yes '[          ]'|head -7|cat - m|tail -7>b
cat b
echo $S

The original code before golfing:


mkpieces() {
    pieces=('r@j.j.j.' '2r@jh.' '2r@j.' '3r@jh1.' 'r@j.j2.' 'l2r@j2h.' 'lr@j.jh2.')
    letters=(I O Z T L S J)

    for j in `seq 0 9`; do
        for i in `seq 0 6`; do
            echo "jk$(($j+1))l${pieces[$i]}:wq! temp" > ${letters[$i]}$j

counthashes() {
    tr -cd '#' < $1 | wc -c

droppiece() {
    for y in `seq 1 5`; do
        echo -n $y | cat - $1 | vi board > /dev/null
        egrep '={10}' temp > /dev/null || break
        [ `counthashes board` -eq `counthashes temp` ] || break
        tr @ "#" < temp > newboard
    cp newboard board

removelines() {
    egrep -v '#{10}' board > temp
    SCORE=$(($SCORE + 10 * (`wc -l < board` - `wc -l < temp`)))
    yes '[          ]' | head -7 | cat - temp | tail -7 > board

for piece; do
    droppiece $piece
cat board
echo $SCORE

(1) A bash file, decompressing and running vi .. not sure about the legality of such an abomination.. but it is most impressive, +1. kudos to you sir. - Michael Anderson
Takes a ridiculously long time to complete, and then generates the wrong output for test case "T2 Z6 I0 T7 T2 Z6 T2 I5 I1 I0 T4 O8 T1 T6 T3 Z0 I9 I6 O7 T3 I2 O0 J8 L6 O7 O4 I3 J8 S6 O1 I0 O4" (same board as example input). Moreover, thousands of garbage lines are going to stdout when piped, and the result of the board should probably be going there instead. - Nabb
It would be much faster if Elvis were installed instead of Vim as vi. - PleaseStand
(2) @Nabb: I've just fixed all those issues at a cost of only three characters. - PleaseStand
Wow. That's some pretty impressive abuse of bash. - P Daddy
[+13] [2010-10-04 23:41:47] poke

Python: 504 519 chars

(Python 3 solution) Currently requires to set the input in the format as shown at the top (input code is not counted). I'll expand to read from file or stdin later. Now works with a prompt, just paste the input in (8 lines total).

f,p=[input()[1:11]for i in R(7)],p
for(a,b)in input().split():
 t=[' '*int(b)+r+' '*9for r in{'I':'#,#,#,#','O':'##,##','Z':'##, ##','T':'###, # ','L':'#,#,##','S':' ##,##','J':' #, #,##'}[a].split(',')]
 for r in R(6-len(t),0,-1):
  for i in R(len(t)):
   if any(a==b=='#'for(a,b)in zip(t[i],f[r+i])):break
   for i in R(0,len(t)):
    f[r+i]=''.join(a if b!='#'else b for(a,b)in zip(t[i],f[r+i]))
    if f[r+i]=='#'*10:del f[r+i];f[0:0]=[' '*10];p+=10
print('\n'.join('['+r+']'for r in f[:7]),p,sep='\n')

Not sure if I can save much more there. Quite a lot characters are lost from the transformation to bitfields, but that saves a lot more characters than working with the strings. Also I'm not sure if I can remove more whitespace there, but I'll try it later.
Won't be able to reduce it much more; after having the bitfield-based solution, I transitioned back to strings, as I found a way to compress it more (saved 8 characters over the bitfield!). But given that I forgot to include the L and had an error with the points inside, my character count only goes up sigh... Maybe I find something later to compress it a bit more, but I think I'm near the end. For the original and commented code see below:

Original version:

field = [ input()[1:11] for i in range(7) ] + [ 0, input() ]
# harcoded tetrominoes
tetrominoes = {'I':('#','#','#','#'),'O':('##','##'),'Z':('##',' ##'),'T':('###',' # '),'L':('#','#','##'),'S':(' ##','##'),'J':(' #',' #','##')}
for ( f, c ) in field[8].split():
    # shift tetromino to the correct column
    tetromino = [ ' ' * int(c) + r + ' ' * 9 for r in tetrominoes[f] ]

    # find the correct row to insert
    for r in range( 6 - len( tetromino ), 0, -1 ):
        for i in range( len( tetromino ) ):
            if any( a == b == '#' for (a,b) in zip( tetromino[i], field[r+i] ) ):
                # skip the row if some pieces overlap
            # didn't break, insert the tetromino
            for i in range( 0, len( tetromino ) ):
                # merge the tetromino with the field
                field[r+i] = ''.join( a if b != '#' else b for (a,b) in zip( tetromino[i], field[r+i] ) )

                # check for completely filled rows
                if field[r+i] == '#' * 10:
                    # remove current row
                    del field[r+i]
                    # add new row
                    field[0:0] = [' '*10]
                    field[7] += 10
            # we found the row, so abort here
# print it in the requested format
print( '\n'.join( '[' + r + ']' for r in field[:7] ) )
# and add the points = 10 * the number of redundant lines at the end
print( str( field[7] ) )

I don't think this is correct. There's no rule saying that only the bottom line can disappear, but judging by your comments, you only check that line. - Michael Madsen
Please, make your Input like in a task. I mean, input from file or STDIN. - Nakilon
(6) Don't you love how even minified Python code is still fairly readable? - EMP
Meh, that readability increases character count.. :/ - poke
@Evgeny, only if compared with Perl or Malbolge ) - Nakilon
@Evgeny: if this is your idea of "readable" code ...! - Porculus
Well, I meant "readable" relative to other code golf answers! - EMP
(2) Defeated by Perl :_( - helpermethod
Is the ; on line 10 intentional? Also, you can save a bit of space by replacing len() with a 1-character name like you did with range... - Wooble
Oh, no that wasn't intentional, thanks. And no, I can't save space with replacing len: I have 3 len means I save 6 chars by shortening it, but I need 5 characters (L=len) for the definition + line break/other separator. - poke
q=len(t) is going to save you a whopping 7 bytes. - Michael Anderson
@Evgeny: readable, but almost 3x larger than the golfscript - Claudiu
[+13] [2010-10-09 20:08:39] Ventero

Ruby 1.9, 357 355 353 339 330 310 309 chars

e=[*$<]{|f|f="L\003\003\007J\005\005\007O\007\007Z\007\013S\013\007I\003\003\003\003T\017\005"[/#{f[j=0]}(\W*)/,1]{|z|?\0+?\0*f[1].hex+z.to_s(2).tr("01"," #")[1,9]}
k,f,i=i,[p]+f,{|l,m|{|n,o|j|=n&3&q=o||0;(n|q).chr}*""}until j>0
puts e,d

Note that the \000 escapes (including the null bytes on the third line) should be replaced with their actual nonprintable equivalent.

Sample input:

[          ]
[          ]
[          ]
[          ]
[ #    #  #]
[ ## ######]
T2 Z6 I0 T7


ruby1.9 tetris.rb < input


ruby1.9 tetris.rb input

Another way to drop tetrominos and keeping all glass in array, even with borders... nice. Now you'll be the Ruby/Perl leader. P.S.: I didn't know about ?\s. - Nakilon
[+12] [2010-10-07 19:21:48] schnaader

C, 727 [...] 596 581 556 517 496 471 461 457 chars

This is my first code golf, I think character count can get much lower, would be nice if experienced golfers can give me some hints.

The current version can handle playfields with different dimensions, too. The input can have linebreaks in both DOS/Windows and Unix format.

The code was pretty straightforward before optimization, the tetrominoes are stored in 4 integers that are interpreted as an (7*3)x4 bit array, the playfield is stored as-is, tiles are dropped and complete lines are removed at start and after each tile drop.

I wasn't sure how to count characters, so I used the filesize of the code with all unneccessary linebreaks removed.

EDIT 596=>581: Thanks to KitsuneYMG, everything except the %ls suggestion worked perfectly, additionally, I noticed putch instead of putchar can be used (getch somehow doesn't work) and removed all the parentheses in #define G.

EDIT 581=>556: Wasn't satisfied with the remaining for and the nested F loops, so there was some merging, changing and removing of loops, quite confusing but definitely worth it.

EDIT 556=>517: Finally found a way to make a an int array. Some N; merged with c, no break anymore.

EDIT 496=>471: Playfield width and height fixed now.

EDIT 471=>461: Minor modifications, putchar used again as putch is no standard function.

EDIT: Bugfix, complete lines were removed before tile drop instead of after, so complete lines could be left at the end. Fix doesn't change the character count.

#define N (c=getchar())
#define G T[j%4]&1<<t*3+j/4
#define X j%4*w+x+j/4
#define F(x,m) for(x=0;x<m;x++)
#define W while

(1) Can't you define for as #define F(x,m) for(x=0;x++<m;)? It works on C#... :P - BrunoLM
@BrunoLM: Thanks, but this won't work, f.e. F(x,3){printf("%i",x} prints 12 instead of 012 with this change. Could change to for(x=-1;x++<m;), but this doesn't save anything :) - schnaader
@schnaader, printf("%03i",x) ? - Nakilon
@Nakilon: Well, that solves the example I gave BrunoLM, but doesn't solve the problem that for(x=0;x++<m;) skips 0 and so I can't use it in my code :) - schnaader
(1) If you've written the code correctly, if you compile as C you don't need to include stdio.h (unless I missed something?). Save a few chars :) - please delete me
Yeah, #include directives are redundant here. - Michael Foukarakis
@brune, Michael: Thanks, edited, less than 600 now :) - schnaader
If you are compiling with gcc, you can make a[99] an int (a move it to the above line with a comma) and use %ls to printf it. - KitsuneYMG
Unless I'm way off if(c==10) could be if(c<11) since none of the characters under 9 are valid as input - KitsuneYMG
(1) You can replace your define of N with (c=getchar()) and remove all c=N lines saving 6 chars. Unless I'm wrong about these, you should get t down to 585 - KitsuneYMG
May I add that putch is a non-standard function. - user191776
This code doesn't work. There are complete lines in the output when using Nabb's test case ("T2 Z6 I0 T7 T2 Z6 T2 I5 I1 I0 T4 O8 T1 T6 T3 Z0 I9 I6 O7 T3 I2 O0 J8 L6 O7 O4 I3 J8 S6 O1 I0 O4", same board as example input). - PleaseStand
@idealmachine: Thanks for spotting this bug, complete lines were removed before instead of after each tile drop, fixed now, doesn't change character count. Output for Nabb's case looks good now, score 120. - schnaader
(1) type defaults to int also for variables, at least for C89. - ninjalj
@ninjalj: Wow, didn't know that (although I guess that's why the main() construct works), thanks! - schnaader
[+8] [2010-10-12 13:25:45] P Daddy

Python 2.6+ - 334 322 316 characters

397 368 366 characters uncompressed

exec'xÚEPMO!½ï¯ i,P*Ýlš%ì­‰=‰Ö–*†­þz©‰:‡—Lò¾fÜ”bžAù,MVi™.ÐlǃwÁ„eQL&•uÏÔ‹¿1O6ǘ.€LSLÓ’¼›î”3òšL¸tŠv[ѵl»h;ÁºŽñÝ0Àë»Ç‡ÛûH.ª€¼âBNjr}¹„V5¾3Dë@¼¡•gO. ¾ô6 çÊsÃЮürÃ1&›ßVˆ­ùZ`Ü€ÿžcx±ˆ‹sCàŽ êüRô{U¯ZÕDüE+³ŽFA÷{CjùYö„÷¦¯Î[0þøõ…(Îd®_›â»E#–Y%’›”ëýÒ·X‹d¼.ß9‡kD'.decode('zip')

The single newline is required, and I've counted it as one character.

Browser code-page mumbo jumbo might prevent a successful copy-and-paste of this code, so you can optionally generate the file from this code:

s = """
23 63 6F 64 69 6E 67 3A 6C 31 0A 65 78 65 63 27 78 DA 45 50 4D 4F 03 21
10 BD EF AF 20 69 2C 50 2A 02 DD 6C 9A 25 EC AD 07 8D 89 07 3D 89 1C D6
96 2A 86 05 02 1B AD FE 7A A9 89 3A 87 97 4C F2 BE 66 DC 94 62 9E 41 F9
2C 4D 56 15 69 99 0F 2E D0 6C C7 83 77 C1 16 84 65 51 4C 26 95 75 CF 8D
1C 15 D4 8B BF 31 4F 01 36 C7 98 81 07 2E 80 4C 53 4C 08 D3 92 BC 9B 11
EE 1B 10 94 0B 33 F2 9A 1B 4C B8 74 8A 9D 76 5B D1 B5 6C BB 13 9D 68 3B
C1 BA 8E F1 DD 30 C0 EB BB C7 87 DB FB 1B 48 8F 2E 1C AA 80 19 BC E2 42
4E 6A 72 01 7D B9 84 56 35 BE 33 44 8F 06 EB 40 BC A1 95 67 4F 08 2E 20
BE F4 36 A0 E7 CA 73 C3 D0 AE FC 72 C3 31 26 9B DF 56 88 AD F9 5A 60 DC
80 FF 9E 63 78 B1 88 8B 73 43 E0 8E A0 EA FC 52 F4 7B 55 8D AF 5A 19 D5
44 FC 45 2B B3 8E 46 9D 41 F7 7B 43 6A 12 F9 59 F6 84 F7 A6 01 1F AF CE
5B 30 FE F8 F5 85 28 CE 64 AE 5F 9B E2 BB 45 23 96 59 25 92 9B 94 EB FD
10 D2 B7 58 8B 64 BC 2E DF 39 87 6B 44 27 2E 64 65 63 6F 64 65 28 27 7A
69 70 27 29

with open('', 'wb') as f:
    f.write(''.join(chr(int(i, 16)) for i in s.split()))



[          ]
[          ]
[          ]
[          ]
[ #    #  #]
[ ## ######]
T2 Z6 I0 T7

Newlines must be Unix-style (linefeed only). A trailing newline on the last line is optional.

To test:

> python < intetris
[          ]
[          ]
[          ]
[#      ###]
[#     ### ]
[##### ####]

This code unzips the original code, and executes it with exec. This decompressed code weighs in at 366 characters and looks like this:

import sys
for l in r.pop().split():
 n=int(l[1])+1;i=0xE826408E26246206601E>>'IOZTLSJ'.find(l[0])*12;m=min(zip(*r[:6]+[a])[n+l].index('#')-len(bin(i>>4*l&31))+3for l in(0,1,2))
 for l in range(12):
  if i>>l&2:c=n+l/4;o=m+l%4;r[o]=r[o][:c]+'#'+r[o][c+1:]
 while a in r:s+=10;r.remove(a);r=p+r

Newlines are required, and are one character each.

Don't try to read this code. The variable names are literally chosen at random in search of the highest compression (with different variable names, I saw as much as 342 characters after compression). A more understandable version follows:

import sys

board = sys.stdin.readlines()
score = 0
blank = board[:1] # notice that I rely on the first line being blank
full  = '[##########]\n'

for piece in board.pop().split():
    column = int(piece[1]) + 1 # "+ 1" to skip the '[' at the start of the line

    # explanation of these three lines after the code
    bits = 0xE826408E26246206601E >> 'IOZTLSJ'.find(piece[0]) * 12
    drop = min(zip(*board[:6]+[full])[column + x].index('#') -
               len(bin(bits >> 4 * x & 31)) + 3 for x in (0, 1, 2))

    for i in range(12):
        if bits >> i & 2: # if the current cell should be a '#'
            x = column + i / 4
            y = drop + i % 4
            board[y] = board[y][:x] + '#' + board[y][x + 1:]

    while full in board:      # if there is a full line,
        score += 10           # score it,
        board.remove(full)    # remove it,
        board = blank + board # and replace it with a blank line at top

print ''.join(board), score

The crux is in the three cryptic lines I said I'd explain.

The shape of the tetrominoes is encoded in the hexadecimal number there. Each tetronimo is considered to occupy a 3x4 grid of cells, where each cell is either blank (a space) or full (a number sign). Each piece is then encoded with 3 hexadecimal digits, each digit describing one 4-cell column. The least significant digits describe the left-most columns, and the least significant bit in each digit describes the top-most cell in each column. If a bit is 0, then that cell is blank, otherwise it's a '#'. For example, the I tetronimo is encoded as 00F, with the four bits of the least-significant digit set on to encode the four number signs in the left-most column, and the T is 131, with the top bit set on the left and the right, and the top two bits set in the middle.

The entire hexadecimal number is then shift one bit to the left (multiplied by two). This will allow us to ignore the bottom-most bit. I'll explain why in a minute.

So given the current piece from the input, we find the index into this hexadecimal number where the 12 bits describing it's shape begin, then shift that down so that bits 1–12 (skipping bit 0) of the bits variable describe the current piece.

The assignment to drop determines how many rows from the top of the grid the piece will fall before landing on other piece fragments. The first line finds how many empty cells there are at the top of each column of the playing field, while the second finds the lowest occupied cell in each column of the piece. The zip function returns a list of tuples, where each tuple consists of the nth cell from each item in the input list. So, using the sample input board, zip(board[:6] + [full]) will return:

 ('[', '[', '[', '[', '[', '[', '['),
 (' ', ' ', ' ', ' ', ' ', ' ', '#'),
 (' ', ' ', ' ', ' ', '#', '#', '#'),
 (' ', ' ', ' ', ' ', ' ', '#', '#'),
 (' ', ' ', ' ', ' ', ' ', ' ', '#'),
 (' ', ' ', ' ', ' ', ' ', '#', '#'),
 (' ', ' ', ' ', ' ', ' ', '#', '#'),
 (' ', ' ', ' ', ' ', '#', '#', '#'),
 (' ', ' ', ' ', ' ', ' ', '#', '#'),
 (' ', ' ', ' ', ' ', ' ', '#', '#'),
 (' ', ' ', ' ', ' ', '#', '#', '#'),
 (']', ']', ']', ']', ']', ']', ']')

We select the tuple from this list corresponding to the appropriate column, and find the index of the first '#' in the column. This is why we appended a "full" row before calling zip, so that index will have a sensible return (instead of throwing an exception) when the column is otherwise blank.

Then to find the lowest '#' in each column of the piece, we shift and mask the four bits that describe that column, then use the bin function to turn that into a string of ones and zeros. The bin function only returns significant bits, so we need only calculate the length of this string to find the lowest occupied cell (most significant set bit). The bin function also prepends '0b', so we have to subtract that. We also ignore the least significant bit. This is why the hexadecimal number is shift one bit to the left. This is to account for empty columns, whose string representations would have the same length as a column with only the top cell full (such as the T piece).

For example, the columns of the I tetromino, as mentioned earlier, are F, 0, and 0. bin(0xF) is '0b1111'. After ignoring the '0b', we have a length of 4, which is correct. But bin(0x0) is 0b0. After ignoring the '0b', we still have a length of' 1, which is incorrect. To account for this, we've added an additional bit to the end, so that we can ignore this insignificant bit. Hence, the +3 in the code is there to account for the extra length taken up by the '0b' at the beginning, and the insignificant bit at the end.

All of this occurs within a generator expression for three columns ((0,1,2)), and we take the min result to find the maximum number of rows the piece can drop before it touches in any of the three columns.

The rest should be pretty easy to understand by reading the code, but the for loop following these assignments adds the piece to the board. After this, the while loop removes full rows, replacing them with blank rows at the top, and tallies the score. At the end, the board and score are printed to the output.

awesome compression - glebm
[+6] [2010-10-15 08:55:58] Nas Banov

Python, 298 chars

Beats all non- esoteric language [1] solutions so far (Perl, Ruby, C, bash...)

... and does not even use code-zipping chicanery.

import os
for k,v in r(0,99).split():
    t=map(ord,' -:G!.:; -:; !-.!"-. !". !./')['IJLOSTZ'.find(k)*4:][:4];v=int(v)-31
    while'!'>max(b[v+j+13]for j in t):v+=13
    for j in t:b=b[:v+j]+'#'+b[v+j+1:]
print b[-91:],1060-10*len(b)/13

On the test example

[          ]
[          ]
[          ]
[          ]
[ #    #  #]
[ ## ######]
T2 Z6 I0 T7

it outputs

[          ]
[          ]
[          ]
[#      ###]
[#     ### ]
[##### ####]

PS. fixed a bug pointed out by Nakilon at cost of +5


That's some pretty impressive code. It would take 14 more characters to fix it (, unless there's a better way, but even still, it beats everything but GolfScript and Bash. It's certainly a clever solution. - P Daddy
Yes, absolutely a very fine solution! - ChristopheD
@Nakilon: thanks, obviously missed that one. fixed @ cost 293->298 - Nas Banov
@P Daddy, thanks - i found a way to bring the fix under bash&toolchain to keep me honest in saying "all non-esoteric" :) - Nas Banov
@Nabb: to keep the code shorter, it was written with some limitations in mind. something like 33 tetrominos max and 99 line drops max. Can easily be extended for the price of +3. Or for a low-low price :), the limitations can be lifted altogether. BTW, this is a great example of how having a test set would have clarified the spec (what i was bugging ChristopherD in comments) - Nas Banov
[+5] [2010-10-07 22:39:26] coderaj

Golfscript 260 chars

I'm sure this could be improved, I'm kind of new to Golfscript.

[39 26.2/0:$14{.(}:?~1?15?1?14 2??27?13.!14?2?27?14 1]4/:t;n/)\n*:|;' '/-1%.,:c;~{)18+:&;'XIOZTLSJX'\%~;,1-t\={{.&+.90>{;.}*|\=32=!{&13-:&;}*}%}6*{&+}/|{\.@<'#'+\)|>+}4*{'['\10*']'++}:
={;)}{n+|+:|;}if\.}do;' '

End of lines are relevant (there shouldn't be one at the end). Anyway, here are some of the test cases I used:

> cat init.txt 
[          ]
[          ]
[          ]
[          ]
[ #    #  #]
[ ## ######]
T2 Z6 I0 T7> cat init.txt | ruby golfscript.rb tetris.gsc
[          ]
[          ]
[          ]
[#      ###]
[#     ### ]
[##### ####]

> cat init.txt
[          ]
[          ]
[          ]
[          ]
[ #    #  #]
[ ## ##### ]
I0 O7 Z1 S4> cat init.txt | ruby golfscript.rb tetris.gsc
[          ]
[          ]
[          ]
[#         ]
[###  #### ]
[### ##### ]

> cat init.txt
[          ]
[          ]
[          ]
[ ## ###   ]
[ #    #   ]
[ ## ######]
T7 I0 I3> cat init.txt | ruby golfscript.rb tetris.gsc
[          ]
[          ]
[          ]
[          ]
[#  #      ]
[## #  # # ]

Note that there is no end of line in the input file, an end of line would break the script as is.

(2) /me considers GolfScript not to be a true contest language... It's just a library, shaped straight to golf tasks... This library's size may be added to size of golfscript code... - Nakilon
@coderaj: now you've raised the bar too much, I can't compete with that :) - ninjalj
(4) @Nakilon - Can't you say something similar about anything that isn't written in raw machine language? :) The Python interpreter is just a library, add it's size to your entry. </sarcasm> - bta
@Nakilon That's the interpreter. - Nabb
@coderaj: Cleaned up your code for you. Replaced a couple variable names: b to |, o to &. Moved variable a initialization earlier. Removed some whitespace: n 10 to n.(, 15 14 to 15.(, etc. Didn't actually bother understanding the code though :P - Nabb
Aaaand a few more changes - instead of manually putting in the array 'delimiters', just make a big array and split into parts of size 4. Defined .( as ? to save characters. Enough of your code, off to write my own now :P - Nabb
(2) @Nakilon: that's just the interpreter. It could be written in any other language; would you still say that Golfscript is not a real language? - Michael Foukarakis
(1) @Nabb: Thanks, I figured there were some tricks I missed... Don't feel bad, I didn't bother understanding my code either :). - coderaj
(1) @Michael Foukarakis, I can in 1 minute write my own interpreter to solve this task in one char, so what? - Nakilon
I don't think reductio ad absurdum serves any purpose here. Please refer to meta for code-golf, golfscript, etc. - Michael Foukarakis
@Nakilon: golfscript isn't just a library, it's also designed in a way to be terse, that is, stack-based with implicit parameters. writing terse code in it is quite the exercise. if anything, J should be disqualified before golfscript because its library is more extensive. golfscript has no real special "do exactly this" builtins, just general-purpose operators that are very terse and handy. - Claudiu
@Nakilon, If it were remotely believable that you have a clue what golfscript is, I would take offence from your comments, but it isn't so I won't - John La Rooy
I think some people are missing the point here. It doesn;t matter what you use. Reductio adsurdum emans only assembly is allowed. therefor you are reduced to beign entertained by a variety of languages - John Nicholas
[+4] [2010-10-08 04:40:46] jessicah

O'Caml 809 782 Chars

open String let w=length let c s=let x=ref 0in iter(fun k->if k='#'then incr x)s;!x open List let(@),g,s,p,q=nth,ref[],ref 0,(0,1),(0,2)let l=length let u=Printf.printf let rec o x i j=let a=map(fun s->copy s)!g in if snd(fold_left(fun(r,k)(p,l)->let z=c(a@r)in blit(make l '#')0(a@r)(i+p)l;if c(a@r)=z+l then r+1,k else r,false)(j-l x+1,true)x)then g:=a else o x i(j-1)and f x=let s=read_line()in if s.[1]='='then g:=rev x else f(sub s 1 10::x)let z=f [];read_line();;for i=0to w z/3 do o(assoc z.[i*3]['I',[p;p;p;p];'O',[q;q];'Z',[q;1,2];'T',[0,3;1,1];'L',[p;p;q];'S',[1,2;q];'J',[1,1;1,1;q]])(Char.code z.[i*3+1]-48)(l!g-1);let h=l!g in g:=filter(fun s->c s<>w s)!g;for i=1to h-(l!g)do incr s;g:=make 10' '::!g done;done;iter(fun r->u"[%s]\n"r)!g;u"[==========]\n";u"%d\n"(!s*10)

[+4] [2010-10-09 18:18:19] lpetru

Common Lisp 667 657 645 Chars

My first attempt at code golf, so there are probably many tricks that I don't know yet. I left some newlines there to keep some residual "readability" (I counted newlines as 2 bytes, so removing 6 unnecessary newlines gains 12 more characters).

In input, first put the shapes then the field.

(let(b(s 0)m(e'(0 1 2 3 4 5 6 7 8 9)))
(labels((o(p i)(mapcar(lambda(j)(+ i j))p))(w(p r)(o p(* 13 r)))(f(i)(find i b))
(a(&aux(i(position(read-char)"IOZTLSJ")))(when i(push(o(nth i'((0 13 26 39)(0 1 13 14)(0 1 14 15)(0 1 2 14)(0 13 26 27)(1 2 13 14)(1 14 26 27)))(read))m)(a))))
(a)(dotimes(i 90)(if(find(read-char)"#=")(push i b)))(dolist(p(reverse m))
(setf b`(,@b,@(w p(1-(position-if(lambda(i)(some #'f(w p i)))e)))))
(dotimes(i 6)(when(every #'f(w e i))(setf s(1+ s)b(mapcar(lambda(k)(+(if(>(* 13 i)k)13(if(<=(* 13(1+ i))k)0 78))k))b)))))
(dotimes(i 6)(format t"[~{~:[ ~;#~]~}]
"(mapcar #'f(w e i))))(format t"[==========]


T2 Z6 I0 T7
[          ]
[          ]
[          ]
[          ]
[ #    #  #]
[ ## ######]
[          ]
[          ]
[          ]
[#      ###]
[#     ### ]
[##### ####]

Not too short, but +1 for ugliness! I imagine that that's what alphabet soup would look like if it came with parentheses. - P Daddy
@P Daddy: Thanks. Yes, that's probably how it would look like :). - lpetru
[+2] [2010-10-08 14:57:51] Mongus Pong

Ruby 505 479 474 442 439 426 chars

A first attempt. Have done it with IronRuby. I'm sure it can be improved, but I really should get some work done today!

t=[*$<]{|a|g=0;{|b|g+=2**b if t[6-b][a+1]==?#};g}{|x|w,y=[15,51,306,562,23,561,113]["IOZTLSJ"=~/#{x[0]}/],x[1].to_i{|d|r.inject{|b,c|f[d+y]&(w>>(d*4)&15-c+1)>0?c:b}}.max{|b|f[b+y]|=w>>(b*4)&15-l}{i=f.inject{|a,b|a&b};!{|a|b=i^(i-1);a=((a&~b)>>1)+(a&(b>>1))};s+=i>0?10:0}}{|a|{|b|t[6-b][a+1]=f[a]&2**b>0??#:' '}}
puts t,s


cat test.txt | ruby tetris.rb
[          ]
[          ]
[          ]
[          ]
[#      ###]
[#     ### ]
[##### ####]

Edit Now using normal ruby. Got the walls output..

One more Rubist, nice! But make a glass around bricks. - Nakilon
[+1] [2010-10-08 05:28:43] glebm

Another one in Ruby, 573 546 characters


Z={I:?#*4,J:'#,###',L:'###,#',O:'##,##',S:'#,##, #',Z:' #,##,#',T:' #,##, #'}
a = T[R[t].join.scan /.#{'(\D)'*10}.$/]
t,o=Z[z[0].to_sym].split(',').map{|x|x.split //},z[1].to_i
r=0..t.size-1{|u|1+a[o+u].rindex(?#).to_i-t[u].count(' ')}.max
a.each{|x|s=a.max_by(&:size).size;x[s-=1]||=' 'while s>0}
puts (0..8-a.size).map{?[+' '*10+?]},a,s


cat test.txt | ruby 3858384_tetris.rb
[          ]
[          ]
[          ]
[          ]
[#      ###]
[#     ### ]
[##### ####]

Fixed with a.each{|x|s=a.max_by(&:size).size;x[s-=1]||=' 'while s>0} - glebm