10
\$\begingroup\$

Note: This is related to a variation of the game Rummikub


Background & Rules

Rummikub is a tile-based game. There are four colors: red, orange, blue and black. For each color there are 13 tiles (labeled from 1 to 13), and there are also 2 Jokers which are color-independent, hence there are 54 pieces in total. In this variation of Rummikub, each player receives 14 tiles and must get one more tile and drop another one each round, such that the tile count is constant. The players don't see each other's tiles. The goal is to group the tiles, such that all pieces belong to at least one group (see below). When a player has all the pieces grouped, they drop their tile board and reveal their pieces. The others then check if all the combinations are valid, and if they are, the player wins the round.

How can the tiles be grouped?

There are only two types of groups:

  • Multi-color groups:

    • They consist of 3 or 4 tiles.
    • They only contain tiles with the same number on them.
    • All the tiles are of different colors.
    • Example: RED 9, BLUE 9, BLACK 9.
  • Mono-color groups:

    • They consist of at least 3 tiles.
    • They cannot contain more than 13 tiles.
    • They only contain tiles with different, consecutive numbers on them, in ascending order.
    • All the tiles have the same color.
    • Tiles labeled with 1 may not be places after tiles labeled 13.
    • Example: RED 5, RED 6, RED 7.

Wait, what do the Jokers do?

Jokers can substitue any piece in the game. For example, our first example can become JOKER, BLUE 9, BLACK 9, RED 9, JOKER, BLACK 9 or RED 9, BLUE 9, JOKER. The same applies to our other example. However, one may not place two Jokers in the same group, so things like JOKER, ORANGE 8, JOKER are forbidden.


Task

Given a Rummikub tile group, determine whether it is valid. You are guaranteed that no duplicate tiles will appear, except for the 2 jokers and that the tiles you receive as input are valid (eg. things like 60 will not appear).

Input / Output

You may take input and provide the output by any standard method.

Some valid input formats: list of strings, list of tuples, nested lists, strings, or anything else you find suitable. The colors can be taken as Strings (e.g: "Blue","Red", etc.), as String abbreviations (please make Blue and Black tiles distinguishable), or as integers coresponding to a color. When it comes to Jokers, you should mention the way your program receives them as input. If you choose Strings, you may have something like RED 9, JOKER, ..., if you choose tuples you can have (9,"RED"), ("JOKER") or something equivalent. If it helps, you may receive a color for that Joker (which should not affect the output of your program). For example, you may have ("JOKER","RED") or ("JOKER","BLUE"), but that should not influence the output in any way.

Regarding the output, standard rules for a apply.

Worked examples

Let's take an example, that would hopefully make it easier to understand. Given a group as follows, where each tuple represents a tile:

[(9, "RED"), (9, "ORANGE"), ("JOKER"), (9, "BLACK")]

This should return a truthy value, because the input is valid. In this case, the Joker substitutes (9, "BLUE"), and they form a multi-color group.

If you would be given the following group:

[(9, "BLUE"), (9, "ORANGE"), (9, "RED"), (9, "BLACK"), ("JOKER")]

It would be invalid, and thus you program should return a falsy value, because there is nothing left for the joker to substitute, because the maximum number of cards in a multi-color group is 4.

Additional Test Cases

These are for an extended test suite that cover nearly all possible situations:

Input -> Output 

[(1,"BLUE"),(2,"BLUE"),(3,"BLUE"),(4,"BLUE"),(5,"BLUE"), (6,"BLUE")] -> truthy

[(6,"BLUE"),(6,"RED"),(6,"BLACK)] -> truthy

[(5,"BLACK"),(6,"BLACK"),(7,"BLACK"),(8,"BLACK"),(9,"BLACK"),(10,"BLACK"),("JOKER"),(12,"BLACK")] -> truthy 

[("JOKER"),(3,"BLUE"),(3,"RED")] -> truthy

[(8,"BLACK"),(2,"RED"),(13,"BLUE")] -> falsy

[(4,"RED"), (3,"RED"), (5,"RED")] -> falsy

[(5,"BLACK"), (6, "BLACK)] -> falsy

[("JOKER"),(5,"RED"),("JOKER")] -> falsy

[(4,"RED"),(5,"RED"),(6, BLUE")] -> falsy

[(4,"RED"),("JOKER"),(5,"RED")] -> falsy

[(12,"BLACK"),(13,"BLACK),(1,"BLACK")] -> falsy

This is , so the shortest code in bytes in every language wins!

\$\endgroup\$
  • \$\begingroup\$ Related \$\endgroup\$ – Peter Taylor Jul 12 '17 at 11:00
  • \$\begingroup\$ Stealing is the best part of rummikub. Even without that this looks like a fun challenge. \$\endgroup\$ – Josiah Jul 12 '17 at 15:35
  • \$\begingroup\$ Is [] a valid input? \$\endgroup\$ – V. Courtois Jul 12 '17 at 17:36
  • \$\begingroup\$ @V.Courtois Of course. \$\endgroup\$ – Mr. Xcoder Jul 12 '17 at 17:40
  • 1
    \$\begingroup\$ @V.Courtois one may not place two Jokers in the same group, so two an input containing 2 Jokers is falsy. \$\endgroup\$ – Mr. Xcoder Jul 12 '17 at 18:09
6
\$\begingroup\$

APL (Dyalog), 58 bytes

Takes list of colors (1-4) as right argument and list of numbers as left argument. A Joker's number is denoted (⍳4) which is equivalent to (1 2 3 4) to indicate that it could be any of those. Likewise, its color is denoted (⍳13) to indicate that it could be any of the numbers from 1 through 13.

{(3≤≢⍺)∧((s⍵)∧⍺≡∪⍺)∨((s←{1∊≢∘∪¨⊃,¨/⍵})⍺)∧∨/∊(⊃,¨/⍵)⍷¨⊂⍳13}

Try it online!

Algorithm

There are three conditions, of which the last two have two conditions each:

  1. The run must have length greater than or equal to 3

AND EITHER

    1. a single number AND

    2. unique colors

OR

    1. a single color AND
    2. sequential numbers

in order for the run to be valid.

Reading order

3≤ 3 is less than or equal to the ≢⍺ number of tiles

and

   s⍵ all numbers are the same

   and

   ⍺≡∪⍺ the colors are unique

or

   1∊ 1 is among ≢∘∪¨ the number of unique ⊃,¨/ expanded  colors

   and

   ∨/ there exists at least one among all ⊃,¨/⍵ expanded number runs ⍷¨⊂ one which is found in ⍳13 1 through 13

Full code explanation

{} anonymous function where is left argument and is right argument

3.2.

⍳13 the numbers 1 through 13

()⍷¨ find the starting positions of each of the following runs:

  ,¨/⍵ join each element of the numbers (creates a run for each Joker value)

   disclose (because / reduces rank)

  ϵnlist (flatten)

∨/ OR reduction (i.e. are any true?)

()∧ AND:

3.1

  ()⍺ the result of applying the following function on the list of colors:

   s←{}s (for same) which is the following anonymous function ( is its argument):

    ,¨/⍵ join each element across (creates a run for each Joker value)

     disclose (because / reduces rank)

    ≢∘∪¨ the the number of unique elements in each list

    1∊ is one a member? (i.e. are there any all-same lists?)

()∨OR:

2.2.

  ∪⍺ the unique colors

  ⍺≡ are identical to the colors (i.e. they are unique)

  ()∧ AND:

2.1.

   s⍵ the numbers are all the same

  ()∧AND

1.

   ≢⍺ the number of colors (i.e. the number of tiles)

   3≤ three is less than or equal to that

\$\endgroup\$
  • 1
    \$\begingroup\$ Wow, it looks like APL is a great tool for this challenge \$\endgroup\$ – Mr. Xcoder Jul 12 '17 at 15:05
3
\$\begingroup\$

Jelly, 41 40 38 36 bytes

EȧI=1ȦȯE
0,W€yµZç/ɓQ⁼⁸ȧ
L>2ȧ4p13ðç€Ṁ

Try it online! (comes with a test-suite footer)

Takes input as an array of (color, value) for regular tiles and 0 for jokers. Colors are represented as integers (although I'm not sure if that even matters for the current code).

Outputs 1 (truthy) or 0 (falsy).

Explanation

L>2ȧ4p13ðç€Ṁ    Main link, checks if a sequence is valid. Args: sequence
L                 Get the length of the sequence.
 >2               Check if it's at least 3 tiles.
   ȧ4             And: yield 4 if it is, 0 otherwise.
     p13          Cartesian product: yield all possible tiles if
                  result was 4, empty array otherwise.
        ð         Begin a new dyadic chain with args (tiles, sequence).
         ç€       Call the first helper link for each tile with args (tile, sequence).

0,W€yµZç/ɓQ⁼⁸ȧ    First helper link, checks if a sequence is valid if jokers
                  are substituted for the given tile. Args: tile, sequence
0,                  Make a pair [0, tile].
  W€                Turn that into [[0], [tile]].
    y               Map all 0's (jokers) into tile in the sequence.
     µ              Begin a new monadic chain with args (sequence).
      Z             Transpose to get list [colors, values].
       ç/           Call the second helper link with args (colors, values).
         ɓ          Begin a new dyadic chain with args (sequence, valid).
          Q         Remove duplicate tiles from the sequence.
           ⁼⁸       Check if the sequence is unchanged (i.e. there were no duplicates).
             ȧ      And with the output of the second helper.

EȧI=1ȦȯE    Second helper link, checks if a sequence is valid assuming no duplicates.
            Args: colors, values
E             Check if all the colors are the same.
 ȧ            Logical and with the values array.
              Yields the values if they were, 0 if not.
  I           Find the differences between each value.
              Yields [] if the colors differed.
   =1         See if each difference is equal to 1.
              Yields [] if the colors differed.
     Ȧ        Check if the list was nonempty and all values were truthy.
              Yields 1 for valid mono-colors, 0 otherwise.
      ȯ       Logical or with the values array.
              Yields 1 for valid mono-colors, the values otherwise.
       E      Check if all the values are the same. For valid mono-colors
              this tests if all items of [1] are equal (obviously true).
              Yields 1 for valid sequences, 0 otherwise.
\$\endgroup\$
  • \$\begingroup\$ I think you have to output a consistent truthy/falsy. \$\endgroup\$ – Adám Jul 12 '17 at 13:23
  • \$\begingroup\$ @Adám Edited, fortunately didn't affect byte count. \$\endgroup\$ – PurkkaKoodari Jul 12 '17 at 13:33
2
\$\begingroup\$

Python 2, 371 370 362 341 329 325 bytes

  • @Mr.Xcoder saved 1 byte: str.split() instead of list literal
  • 8 bytes saved: shorthand for len(x)-1
  • 19 bytes saved: J O BK B R for Joker, Orange, Black, Blue, Red literals
  • @Mr.Xcoder saved yet another 12 bytes, Thanks!!
  • Another 4 bytes thanks to @Mr.Xcoder
def f(x):
 j=sum("J"in i for i in x);z=len(x)-1
 if j>1or z<2:return False
 if j<1:return(all(i[0]==x[0][0]for i in x)and sum(i[1]==x[0][1]for i in x)<2)or(all(i[1]==x[0][1]for i in x)and sum(int(x[m+1][0])==int(x[m][0])+1for m in range(z))==z)
 return any(f([[k,(i+1,j)]["J"in k]for k in x])for j in'RBbO'for i in range(13))

Try it online!

\$\endgroup\$
1
\$\begingroup\$

Javascript (ES6), 286 bytes

var testcases = [[{n:1,c:"BLUE"},{n:2,c:"BLUE"},{n:3,c:"BLUE"},{n:4,c:"BLUE"},{n:5,c:"BLUE"}, {n:6,c:"BLUE"}],[{n:6,c:"BLUE"},{n:6,c:"RED"},{n:6,c:"BLACK"}],[{n:5,c:"BLACK"},{n:6,c:"BLACK"},{n:7,c:"BLACK"},{n:8,c:"BLACK"},{n:9,c:"BLACK"},{n:10,c:"BLACK"},{n:0,c:"JOKER"},{n:12,c:"BLACK"}],[{n:0,c:"JOKER"},{n:3,c:"BLUE"},{n:3,c:"RED"}],[{n:8,c:"BLACK"},{n:2,c:"RED"},{n:13,c:"BLUE"}],[{n:4,c:"RED"}, {n:3,c:"RED"}, {n:5,c:"RED"}],[{n:5,c:"BLACK"}, {n:6,c:"BLACK"}],[{n:0,c:"JOKER"},{n:5,c:"RED"},{n:0,c:"JOKER"}],[{n:4,c:"RED"},{n:5,c:"RED"},{n:6,c:"BLUE"}],[{n:4,c:"RED"},{n:0,c:"JOKER"},{n:5,c:"RED"}],[{n:12,c:"BLACK"},{n:13,c:"BLACK"},{n:1,c:"BLACK"}],[{n:11,c:"BLACK"},{n:12,c:"BLACK"},{n:0,c:"JOKER"}],[{n:1,c:"BLACK"},{n:2,c:"BLACK"},{n:3,c:"BLACK"},{n:1,c:"BLUE"},{n:2,c:"BLUE"},{n:3,c:"BLUE"}]];

g=a=>a.length
j=a=>a.n==0
l=(x,y)=>x.c==y.c||j(x)||j(y)
a=s=>g(s)>2&&([q=[0],x=s[0],s.map(y=>q[0]+=x==y||((l(x,y)||x.n==y.n)&&!(j(x)&&j(y)))&&(([n=s.indexOf(y),n<1||([x=s[n-1],!l(x,y)||y.n>0&&x.n<y.n])[1]||(n<g(s)-1&&x.n+1<s[n+1].n)||(n==g(s)-1&&y.n==0&&x.n<13)])[1])?1:0)])[0][0]==g(s)

testcases.forEach(H=>console.log(a(H)));

(Note that the test cases above contain 2 additional test cases that are not in the Question: they are true and false respectively: see the ungolfed version for readability).

Rough process:

 Using first tile x:
   For each tile y:
     count for x: can group with y
 return: x matches n tiles, where n is the number of tiles

Jokers are indicated by having a 0 as their numerical value (a negative number would work too); this keeps the input struct consistent (has both a Color and Value) and doesn't rely on having to check if c=="JOKER", saving 7 bytes.

Its possible that some parentheses could be removed, it might be possible to not box q as an array (I tried it and the value just either stayed 0 or caused nasal demons).

Ungolfed:

var testcases = [
[{n:1,c:"BLUE"},{n:2,c:"BLUE"},{n:3,c:"BLUE"},{n:4,c:"BLUE"},{n:5,c:"BLUE"}, {n:6,c:"BLUE"}],//true
[{n:6,c:"BLUE"},{n:6,c:"RED"},{n:6,c:"BLACK"}],//true
[{n:5,c:"BLACK"},{n:6,c:"BLACK"},{n:7,c:"BLACK"},{n:8,c:"BLACK"},{n:9,c:"BLACK"},{n:10,c:"BLACK"},{n:0,c:"JOKER"},{n:12,c:"BLACK"}],//true
[{n:0,c:"JOKER"},{n:3,c:"BLUE"},{n:3,c:"RED"}],//true
[{n:8,c:"BLACK"},{n:2,c:"RED"},{n:13,c:"BLUE"}],//false
[{n:4,c:"RED"}, {n:3,c:"RED"}, {n:5,c:"RED"}],//false
[{n:5,c:"BLACK"}, {n:6,c:"BLACK"}],//false
[{n:0,c:"JOKER"},{n:5,c:"RED"},{n:0,c:"JOKER"}],//false
[{n:4,c:"RED"},{n:5,c:"RED"},{n:6,c:"BLUE"}],//false
[{n:4,c:"RED"},{n:0,c:"JOKER"},{n:5,c:"RED"}],//false
[{n:12,c:"BLACK"},{n:13,c:"BLACK"},{n:1,c:"BLACK"}],//false
[{n:11,c:"BLACK"},{n:12,c:"BLACK"},{n:0,c:"JOKER"}],//true
[{n:1,c:"BLACK"},{n:2,c:"BLACK"},{n:3,c:"BLACK"},{n:1,c:"BLUE"},{n:2,c:"BLUE"},{n:3,c:"BLUE"}]
];

g=a=>a.length
i=(a,v)=>a.indexOf(v)
j=x=>x.n==0
m=(x,y)=>
       (l(x,y)||x.n==y.n)
    &&!(j(x)&&j(y))
l=(x,y)=>x.c==y.c||j(x)||j(y)
c=(a,v)=>([n=i(a,v),
      n<1
    ||([x=a[n-1],!l(x,v)||v.n>0&&x.n<v.n])[1]
    ||(n<g(a)-1&&x.n+1<a[n+1].n)
    ||(n==g(a)-1&&v.n==0&&x.n<13)])[1]
a=s=>g(s)>2&&([q=[0],x=s[0],s.map(y=>q[0]+=x==y||m(x,y)&&c(s,y)?1:0)])[0][0]==g(s)

testcases.forEach(H=>console.log(a(H)));

Version I worked on to get the logic correct. Single-use lambdas got in-lined; here's their corresponding function:

g() -> string.length
i() -> indexof
j() -> isJoker
m() -> do tiles match
l() -> do colors match
c() -> same-color isConsecutiveOrder
a() -> main lambda
\$\endgroup\$
1
\$\begingroup\$

C# (.NET Core), 198 bytes

using System.Linq;(C,N)=>{int l=C.Length,j=C.Count(x=>x<1),c=C.Distinct().Count(),n=N.Distinct().Count(),u=N.Min();foreach(var x in N)u*=0<(u&x)?2:0;return l>2&((u>0&n==l&c<2+j)|(n<2+j&c==l&l<5));};

Takes in the colors of tiles and numbers on them as separate lists of integers. The specifics of that mapping don't matter as long as each color has a different integer and Jokers are represented as 0.

The format for inputting numbers is pretty special though. The number that needs to be input for a number n is instead 2^n, while the number used to represent a joker should be (2^14)-1. This enables the bitwise and u&x to evaluate to u if the tile x has value equal to u or is a joker.

C# (.NET Core), 200 bytes

using System.Linq;(C,N)=>{int l=C.Length,j=N.Count(x=>x<1),c=C.Distinct().Count(),n=N.Distinct().Count(),u=N.Min();foreach(var x in N)u=u==x|x<1?u+1:0;return l>2&((u>0&n==l&c<2+j)|(n<2+j&c==l&l<5));};

A 2 byte longer solution which isn't eclectic about input. Turns out just using a special case for jokers in the one place they were hard to deal with wasn't much longer than the clever bitwise operation I was so proud of. Here Jokers are (0,0), other numbers are as expected, and colors are represented any 4 values which are distinct from each other by C#'s default comparison (specifically, the Linq Distinct() operation must consider values for the same color as 'not distinct' and values for different colors as 'distinct').

Something which might be of use to other languages, u*=!u++^x*x would be equivalent to u=u==x|x<1?u+1:0 in some languages; u^x is 0 iff u==x, and 0 times any int is 0, so u^x*x would be 0 for either u==x or x==0 if C# didn't make bitwise operations lower precedence than mathematical ones. C# also can't interpret ints as bools without explicit casting. A language that tries harder to make types work might convert the values 0 and not 0 to false and true before applying ! to them though, and then when going back to an int interpret !false as 1 and !true as 0. All that said, I can't guarantee another language would actually benefit from the rest of the algorithm so it might not even come up.

\$\endgroup\$
1
\$\begingroup\$

Scala, 491 477 chars, 491 477 bytes

This challenge was fun; thanks.

var c=Seq("O","B","b","R")
t match{case _ if t.length<3=>false
case _ if t.exists(x=>x._1==0)=>{var b=false
if(t.filter(q=>q._1!=0).exists(q=>q._1==0))b else{for(y<-1 to 13)for(u<-c)b=b|f(t.takeWhile(q=>q._1!=0)++:(y,u)+:t.reverse.takeWhile(q=>q._1!=0).reverse)
b}}
case _::(x,_)::_ if t.forall(_._1==x)=>true
case _ if t.forall(_._2==c(0))|t.forall(_._2==c(1))|t.forall(_._2==c(2))|t.forall(_._2==c(3))=>(t(0)._1 to t(0)._1+t.length-1).toList equals t.map(_._1)
case _=>false}

So f at line 4 is a recursive call where I try replacing "JOKER" by every other tile. See tio for a clearer view of the code. I chose taking as input a sequence of 2-tuples (Int,String) - called t in my code, see tio - so "JOKER" is represented by a 2-tuple (0,"JOKER").

EDIT : 14 bytes saved thanks to comments, I take O B b R for ORANGE BLACK BLUE RED.

Try It Online!

EDIT : -2 bytes, deleted useless ( around conditions of the case _ ifs

\$\endgroup\$
  • \$\begingroup\$ Can't you use O,B,b,R instead of ORANGE,BLUE,BLACK,RED to save bytes? I have no idea how Scala works, but I think you can. \$\endgroup\$ – Mr. Xcoder Jul 13 '17 at 19:00
  • \$\begingroup\$ I tried; in fact it saves bytes doing this way (a seq of strings). It does var (O,B,b,R)=("ORANGE","BLACK","BLUE","RED") and calls are O B b R, for a total of 49 bytes; where var c=Seq("ORANGE","BLACK","BLUE","RED") and calls c(...) totalize 58 bytes. BUT the first case permits for(u<-c) in place of for(u<-Seq(O,B,b,R)), so the cost is not -9 but +2. Thanks for trying though. \$\endgroup\$ – V. Courtois Jul 13 '17 at 19:58
  • \$\begingroup\$ @V.Courtois I believe what Mr. Xcoder was suggesting is using var c=Seq("O","B","b","R") and taking those characters as your inputs rather than full strings for color. As mentioned in the original post, "The colors can be taken as ... String abbreviations". \$\endgroup\$ – Kamil Drakari Jul 14 '17 at 21:28
  • \$\begingroup\$ ohh~ i see what you mean, thanks @ both of you \$\endgroup\$ – V. Courtois Jul 14 '17 at 22:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.