Is this platform game level winnable?

23

2

Given a level from a simple platform game, your task is to make a program or function to determine if a level is winnable. Platform game levels are 4 characters tall and any number of characters wide. There is exactly one platform for each horizontal space in a level:

           =======    =     
==    =           =         
   ===                 =====
  =    ====        ===      

The game consists of jumps. Jumps start from the space above the platform which is being jumped from, and end in the space above the platform which is being jumped to (these can be above the 4 platform height levels). To complete a jump, it must be possible to move from the start point to the end point in four or less (including the start/end points) up, down, left, and right moves which do not pass through a platform. Here are some examples (marked with A for start platform and B for end platform):

 >>>
 ^ B
=A
  =
 >>>>
=A  B=
>>
^B
A

Horizontal moves along a platform are just jumps which only move in one direction:

  >>
==AB===

The player would start on the leftmost platform, and wins by standing on the rightmost one (marked with vs):

v  === v
===   ==

Test cases

Winnable:


 ===      ==
=    =====  
    =       
    ==      

=         ==
 ===  ====  
   ==== =   
===         
       =    
         ===
  ==  ==  = 

=           
 =  ==  == =

Unwinnable:

      ======


======      
 =======   =
=         =
         =
        =
           =
   =        
=     =     
 == == ==== 

Rules

Your program should output one distinct value if a level is winnable, and a different one if it isn't. It can take input in any reasonable form, such as an ASCII art with any two distinct characters, a matrix, or a 2d array.

Code golf, so shortest answer per language wins.

Redwolf Programs

Posted 2019-12-02T23:45:14.757

Reputation: 2 561

The second test case has a gap -- is that allowed? May we take input column by column? – xnor – 2019-12-02T23:53:08.533

@xnor The gap is 2 wide, so it is jumpable. You may take input column-by-column. – Redwolf Programs – 2019-12-03T00:04:42.217

1Why is the last case unwinnable? – Nick Kennedy – 2019-12-03T00:07:26.883

@NickKennedy Any jump would be 5 units. Falling is included in the jump limit. – Redwolf Programs – 2019-12-03T00:08:22.490

@xnor Oh, oops, there should be some unreachable platforms above for that one. Thanks for pointing that out. – Redwolf Programs – 2019-12-03T00:10:57.013

@RedwolfPrograms I see - it was the counting of the start that through me. I was expecting it to involve four moves (right, down, down, down). – Nick Kennedy – 2019-12-03T00:11:24.800

3There should be a testcase where the player needs to move left to complete the level – Embodiment of Ignorance – 2019-12-03T04:21:46.337

2Another good pair of test cases would be where platforms alternate between the top and bottom position for a while, and you need to choose the upper or lower path at the start but only one of them gets to the finish. – xnor – 2019-12-03T05:38:46.777

1Is the requirement "which [The moving] do not pass through a platform" really needed? Could anyone provide a testcase which may yield different result when this requirement is removed? – tsh – 2019-12-03T08:20:03.763

3Suggested testcase: $ \begin{bmatrix} &&=&&&=\\=&&&=\&=&&&=\end{bmatrix} $ – tsh – 2019-12-03T08:23:09.440

3Suggested testcase: $ \begin{bmatrix} &=&&&=&&&=&&&=\\\=&&=&=&&=&=&&=&=&& \end{bmatrix} $ – tsh – 2019-12-03T09:07:14.727

@tsh Imagine the last winnable testcase, but the first one is at height 3. You'd be able to jump to the one below, except you'd be stopped by the one in front of you. – Redwolf Programs – 2019-12-03T13:34:17.103

4@tsh The "[moves] which do not pass through a platform" rule can be safely ignored because of the "one platform per column" rule. You can simply jump on any platform that blocks an otherwise legal jump. (In the proposed counterexample, the platform in column 3 does not block a jump from column 1 to column 2.) – Nitrodon – 2019-12-03T18:34:48.343

@Nitrodon But by jumping to the one on level 4 it is impossible to win – Redwolf Programs – 2019-12-03T18:56:52.063

@RedwolfPrograms The platform on level 4 doesn't block anything. In that test case, you would jump from the platform on level 3 (column 1) to the platform on level 1 (column 2). – Nitrodon – 2019-12-03T19:02:10.723

@Nitrodon But if it were at level 3 the level 4 one would block you, since you'd have to pass through it – Redwolf Programs – 2019-12-03T19:43:32.743

Suggests simple test case for block test [[0,0,0,1,0], [1,1,1,0,0], [0,0,0,0,0], [0,0,0,0,1]] – AZTECCO – 2019-12-03T23:47:39.803

1Why is the second unwinnable test case unwinnable? Mario would have no problem with it. – Eric Duminil – 2019-12-04T13:22:20.630

Answers

9

Jelly, 23 22 bytes

ŒṪạþ`§<4NA1¦A»/T$¦ÐLṪṀ

Try it online!

New version, which now efficiently handles moves backwards. Takes input as a boolean matrix (row major). Returns 1 for winnable and 0 for not.

Thanks to @AZTECCO for first suggesting using the indices of platforms, and @tsh for a test case that made me rethink my approach.

Explanation

ŒṪ                     | Multidimensional truthy indices of each
  ạþ`                  | Outer table using absolute difference and with the same argument on both sides:
     §                 | Sum innermost lists
      <4               | - Less than 4
        N              | Negate
         A1¦           | Absolute top row
                  ÐL   | Apply the following until no changes:
            A   $¦     | Absolute the rows indicated by the following:
             »/        | - Max of each column
               T       | - Truthy indices (i.e. where there’s a 1 in the column)
                    Ṫ  | Finally take the last row
                     Ṁ | Max

Step b step

(each matrix shown collapsed as a grid, with - = -1):

1. Input

0010
0001
1000
0010
0001
1000

2.ŒṪ

Multidimensional indices.

[[1, 3], [2, 4], [3, 1], [4, 3], [5, 4], [6, 1]]

3. ạþ`§<4N

Convert to initial outer table with -1 for possible moves between columns and 0 where no move possible.

--0-00
--0--0
00--0-
-----0
0-0--0
00-00-

4. A1¦A»/T$¦ÐL

Absolute first row, then iteratively take each column with at least one 1 in it, and absolute the rows at those indices; results after each iteration shown.

Absolute row 1 ->

110100
--0--0
00--0-
-----0
0-0--0
00-00-

-> Absolute rows 1, 2, 4 ->

110100
110110
00--0-
111110
0-0--0
00-00-

-> Absolute rows 1, 2, 3, 4, 5 ->

110100
110110
001101
111110
010110
00-00-

-> Absolute rows 1, 2, 3, 4, 5, 6 ->

110100
110110
001101
111110
010110
001001

5. ṪṀ

Finally, take the max of the last row

1

My previous 61 byte version has explanation in the edit history; thanks to @JonathanAllan for golfing 3 bytes off that one!

Nick Kennedy

Posted 2019-12-02T23:45:14.757

Reputation: 11 829

ṙ1¬a4$+Ʋ€1¦ can be replaced with ^I$1¦%7 saving four bytes (so long as it is OK that a 1 below the starting 5 becomes a 0 - which I think it is, right?) – Jonathan Allan – 2019-12-03T20:07:33.020

(...edit: a 0 below the starting 5 becomes a 1) – Jonathan Allan – 2019-12-03T20:16:54.310

@JonathanAllan Thanks. I originally had something like that, but it means that the player can start by travelling down which isn’t allowed I don’t think it affects the test cases, but might alter edge cases. – Nick Kennedy – 2019-12-03T21:21:04.143

Ah, well in that case I«0^Ʋ1¦%7 would save two; there is probably something shorter. – Jonathan Allan – 2019-12-03T22:26:37.820

Ah - n⁶Żz0I×-4«Ʋ1¦C saves one more :) – Jonathan Allan – 2019-12-03T22:58:06.257

1@JonathanAllan thanks. I’ve yet to find a test case where moving down at the start makes a difference. Still, it does mean we’re sticking to the official rules of the challenge re moving through blocks! – Nick Kennedy – 2019-12-03T23:12:23.277

@tsh my initial short version didn’t check moves backwards. My new version is more efficient and should find all possible solutions. – Nick Kennedy – 2019-12-04T09:07:17.733

5

JavaScript (ES6),  129  126 bytes

Takes input as a binary matrix. Returns either \$0\$ or \$1\$.

f=(a,x=0,m,d=4,g=d=>k=a.findIndex(r=>r[x+d]))=>!~g(1)|[...'1350531'].some(v=>~g(--d)&&m^(M=m|1<<x+d)&&(k-=g``)*k<v&f(a,x+d,M))

Try it online!

How?

Given the current column \$x\$ and a horizontal jump distance \$dx\$, the helper function \$g\$ returns the 0-indexed row of the platform at column \$x+dx\$, or \$-1\$ if \$x+dx\$ is outside the playfield.

g = d =>             // d = dx
  k =                // save the result in k
    a.findIndex(r => // for each row r in a[]:
      r[x + d]       //   test whether r[x + dx] is truthy
    )                // end of findIndex()

Starting with \$x=0\$, the recursive function \$f\$ attempts to find a way to the last column by trying all valid jumps and keeping track of visited columns.

f = (                // f is a recursive function taking:
  a,                 //   a[] = input matrix
  x = 0,             //   x = current column
  m,                 //   m = bit-mask of visited columns
  d = 4,             //   d = dx
  g = …              //   g = helper function (see above)
) =>                 //
  !~g(1) |           // success if g(1) is equal to -1
  [...'1350531']     // list of exclusive upper bounds for dy²
                     // corresponding to dx = +3, +2, +1, 0, -1, -2, -3
  .some(v =>         // for each upper bound v:
    ~g(--d) &&       //   decrement d; success if g(d) is not equal to -1
    m ^ (            //   AND m is different from
      M = m |        //     the new bit-mask M
          1 << x + d //     where the bit corresponding to x + d is set
    ) &&             //   AND
    (k -= g``) * k   //   dy² = (g(d) - g(0))²
    < v &            //   is less than v
    f(a, x + d, M)   //   AND a recursive call at the new position is also true
  )                  // end of some()

Arnauld

Posted 2019-12-02T23:45:14.757

Reputation: 111 334

It is possible that you'll need to move left at some point to win the level. This solution does not account for that. – Nitrodon – 2019-12-03T19:51:36.847

@Nitrodon Thanks for reporting this. Now fixed. – Arnauld – 2019-12-04T09:14:10.720

4

Japt, 42 bytes

Õcð í
o
v
à cá ®pV uWÃd_äÈcY ó x_raÃ<4 Ãr*

Try it

Test 1

Test 2

Test 3

Temporarily fixed by using permutations to allow backwards jumps (really inefficient) , going to try @Nick Kennedy solution soon.

z cð í  // get each column height and pair with column index
o       // save end position and remove from array
v       // same for start pos

à  cá        // combinations of permutations 
  ®pV uWÃ  // restore end and start

d          // return true if any returns true to..
 _  ...  Ãr* // reduce result
  ä          // pass each consecutive x,y
   ÈcY ó     // 'reshape~transpose' => [ platform pair, index pair]
         x_raà // sum of absolute difference of each pair
               <4 // return if less for each pair

Saved 2 thanks to @Embodiment of Ignorance

Japt -Q, 76 bytes

Õcð í ïÈcY ó x_raÃ<4î*JÃòUÎl
g0_ma
@T=Uc x UgUy_x_>0}Ãð _maÃT¥Uc x}a
o x >0

Try it

Translation of Jelly answer of @Nick Kennedy , a lot more efficient but longer , I think it can be shortened.

Explanation / translation Jelly => Japt (taken from Nick answer)

ŒṪ  => Õcð í        M ultidimensional truthy indices of each
ạþ` => ïÈcY  òUÎl   O uter table using absolute difference and with the same argument on both sides:
§   => ó x_ra       Sum innermost lists
<4  => Ã<4Ã         Less than 4
N   => ®*JÃ         Negate
A1¦ => g0_ma        Absolute top row
ÐL  => @T=Uc x...T¥Uc x}a     Apply the following until no changes: 
   On my side I used T to store the total of the whole matrix to check if it changed because g() modify the original matrix.

A $¦ => Ug..._maà   Absolute the rows indicated by the following:
»/   => Uy_x_>0}    Max of each column : 
   on my side I rotated the table and reduced to get the positive rows
T    => Ãð          Truthy indices (i.e. where there’s a 1 in the column)
Ṫ    => o           Finally take the last row
Ṁ    => x >0        Max : on my side >0

AZTECCO

Posted 2019-12-02T23:45:14.757

Reputation: 2 441

1z -> Õ -1 byte – Embodiment of Ignorance – 2019-12-04T05:31:40.630

1®raÃx <4 -> x_raÃ<4 -1 byte\ – Embodiment of Ignorance – 2019-12-04T05:42:30.097

I think this fails @tsh test case: https://petershaggynoble.github.io/Japt-Interpreter/?v=1.4.6&code=1WPwIO0Kbwp2CuAgrnBWIHVXw2Rf5MhjWSDzIHhfcmHDPDQgw3Iq&input=WwoKW1swLDAsMSwwLDAsMV0sCiBbMCwwLDAsMCwwLDBdLAogWzEsMCwwLDEsMCwwXSwKIFswLDEsMCwwLDEsMF1dCgpdCi1t Your current solution doesn’t allow for moving backwards which is sometimes needed

– Nick Kennedy – 2019-12-04T09:14:07.047

1@Nick Kennedy now I see.. It has to jump backwards to reach higher blocks.. I try to fix it ASAP! thank you – AZTECCO – 2019-12-04T09:45:33.540

1@AZTECCO I don’t know any Japt, but I expect my approach in Jelly could be translated. I build a table of which columns can reach which, and then iteratively extend paths from the first row. – Nick Kennedy – 2019-12-04T12:52:52.857

Thank you @Nick Kennedy , for now I'm managing to fix it using combinations of permutations, although it works it is really inefficient, as I have a moment I surely try to translate your solution which is really interesting – AZTECCO – 2019-12-04T13:59:31.190

@AZTECCO - I tried permutations too. It was fine for the 6 column case, but timed out for the larger cases on TIO – Nick Kennedy – 2019-12-04T14:15:01.137

@Nick Kennedy I finally translated your solution! A bit longer though – AZTECCO – 2019-12-07T13:53:57.750

@AZTECCO looks good. Interesting to see the side-by-side translation too. – Nick Kennedy – 2019-12-07T15:41:10.803

@Nick Kennedy thanks! Idk if it's useful but I loved the idea to compare two different languages, btw did you seen I created a Jelly to Japt chat room ? I tried to ping you , I'm wondering if I should extend it to every language.. If you have a bit of time to look at an give some suggestions.. https://chat.stackexchange.com/rooms/101829/jelly-to-japt

– AZTECCO – 2019-12-07T17:22:39.357

1@AZTECCO nice idea. Sorry I’d not seen it - I think putting the space in my name may have confused the way it picks up usernames. – Nick Kennedy – 2019-12-07T17:24:57.827

2

R, 137 136 bytes

function(a,e=abs(outer(b<-which(a,T),b,`-`)),g=-(e[,1,,1]+e[,2,,2]<4),h=1){while(any(g<{g[h,]=abs(g[h,]);g}))h=colSums(g>0)>0;rev(g)[1]}

Try it online!

Essentially an R translation of my Jelly answer, but with some R golfs to take advantage of the way outer works on arrays. A function that takes as its argument a logical matrix with the rows corresponding to the rows of the original question. Returns 1 for winnable and -1 for non-winnable.

Nick Kennedy

Posted 2019-12-02T23:45:14.757

Reputation: 11 829

1

Python 3, 198 bytes

A messy solution with NumPy, takes a list of strings as input:

w=lambda l:t(g(array(where((array(list(map(list,l)))=='=').T)).T))
from numpy import*
g=lambda p:abs(p[:,None]-p).sum(2)<4
t=lambda m,x=0:x+1==len(m)or max([t(m,p)for p in where(m[x])[0]if p>x]+[0])

Try it online!

This assumes that no levels exist for which the bolded part here is of importance:

To complete a jump, it must be possible to move from the start point to the end point in four or less (including the start/end points) up, down, left, and right moves which do not pass through a platform.

I have been unable to imagine such a level and none are given as a test case. Likewise I can't imagine a level that requires a leftwards move so an appropriate test case there would be welcome also, if I'm mistaken. Thanks to Nitrodon for pointing me in the right direciton.

Python 3, 214 bytes

lambda l:t(g(array(where((array(list(map(list,l)))=='=').T)).T),[])
from numpy import*
g=lambda p:abs(p[:,None]-p).sum(2)<4
t=lambda m,v,x=0:x+1==len(m)or max([t(m,v+[x],p)for p in where(m[x])[0]if x not in v]+[0])

Try it online!

A solution that incorporates leftwards movement, with tsh's test cases included. This cost quite a few bytes, I'll have to sleep on it to golf it more.

Seb

Posted 2019-12-02T23:45:14.757

Reputation: 291

1A 2 up, 1 left move can be useful to reach a higher level. The six-column test case posted by tsh in a comment is an example where this is required. – Nitrodon – 2019-12-03T21:38:39.197

I see it now, thanks! – Seb – 2019-12-04T00:41:15.043