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
1may not be places after tiles labeled13. - 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 decision-problem 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 code-golf, so the shortest code in bytes in every language wins!
-
\$\begingroup\$ Related \$\endgroup\$Peter Taylor– Peter Taylor2017年07月12日 11:00:57 +00:00Commented Jul 12, 2017 at 11:00
-
\$\begingroup\$ Stealing is the best part of rummikub. Even without that this looks like a fun challenge. \$\endgroup\$Josiah– Josiah2017年07月12日 15:35:05 +00:00Commented Jul 12, 2017 at 15:35
-
\$\begingroup\$ Is [] a valid input? \$\endgroup\$V. Courtois– V. Courtois2017年07月12日 17:36:31 +00:00Commented Jul 12, 2017 at 17:36
-
\$\begingroup\$ @V.Courtois Of course. \$\endgroup\$Mr. Xcoder– Mr. Xcoder2017年07月12日 17:40:53 +00:00Commented Jul 12, 2017 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– Mr. Xcoder2017年07月12日 18:09:38 +00:00Commented Jul 12, 2017 at 18:09
6 Answers 6
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}
Algorithm
There are three conditions, of which the last two have two conditions each:
- The run must have length greater than or equal to 3
AND EITHER
a single number AND
unique colors
OR
- a single color AND
- 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
-
1\$\begingroup\$ Wow, it looks like APL is a great tool for this challenge \$\endgroup\$Mr. Xcoder– Mr. Xcoder2017年07月12日 15:05:11 +00:00Commented Jul 12, 2017 at 15:05
Jelly, (削除) 41 (削除ここまで) (削除) 40 (削除ここまで) (削除) 38 (削除ここまで) 36 bytes
EȧI=1ȦȯE
0,W€yμZç/ɓQ=8ȧ
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=8ȧ 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.
=8 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.
-
\$\begingroup\$ I think you have to output a consistent truthy/falsy. \$\endgroup\$Adám– Adám2017年07月12日 13:23:13 +00:00Commented Jul 12, 2017 at 13:23
-
\$\begingroup\$ @Adám Edited, fortunately didn't affect byte count. \$\endgroup\$PurkkaKoodari– PurkkaKoodari2017年07月12日 13:33:21 +00:00Commented Jul 12, 2017 at 13:33
Python 2, (削除) 371 370 362 341 329 (削除ここまで) 325 bytes
- @Mr.Xcoder saved 1 byte:
str.split()instead oflist literal - 8 bytes saved: shorthand for
len(x)-1 - 19 bytes saved:
J O BK B RforJoker, Orange, Black, Blue, Redliterals - @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))
-
2\$\begingroup\$ 370 bytes \$\endgroup\$Mr. Xcoder– Mr. Xcoder2017年07月12日 13:04:39 +00:00Commented Jul 12, 2017 at 13:04
-
\$\begingroup\$ You can replace
BKwithbto save 1 byte (TIO with test cases updated tob. \$\endgroup\$Mr. Xcoder– Mr. Xcoder2017年07月13日 19:05:51 +00:00Commented Jul 13, 2017 at 19:05 -
1\$\begingroup\$ This actually saves far more bytes than I thought: 329. \$\endgroup\$Mr. Xcoder– Mr. Xcoder2017年07月13日 19:11:42 +00:00Commented Jul 13, 2017 at 19:11
-
1\$\begingroup\$ 325 bytes. Sorry for the very late improvement. \$\endgroup\$Mr. Xcoder– Mr. Xcoder2017年08月19日 20:39:46 +00:00Commented Aug 19, 2017 at 20:39
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
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.
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.
EDIT : -2 bytes, deleted useless ( around conditions of the case _ ifs
-
\$\begingroup\$ Can't you use
O,B,b,Rinstead ofORANGE,BLUE,BLACK,REDto save bytes? I have no idea how Scala works, but I think you can. \$\endgroup\$Mr. Xcoder– Mr. Xcoder2017年07月13日 19:00:19 +00:00Commented Jul 13, 2017 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 areOBbR, for a total of 49 bytes; wherevar c=Seq("ORANGE","BLACK","BLUE","RED")and callsc(...)totalize 58 bytes. BUT the first case permitsfor(u<-c)in place offor(u<-Seq(O,B,b,R)), so the cost is not -9 but +2. Thanks for trying though. \$\endgroup\$V. Courtois– V. Courtois2017年07月13日 19:58:05 +00:00Commented Jul 13, 2017 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– Kamil Drakari2017年07月14日 21:28:36 +00:00Commented Jul 14, 2017 at 21:28
Explore related questions
See similar questions with these tags.