13
\$\begingroup\$

Giving a challenge involving a Star Trek reference just after May the 4th may be frowned upon, but here goes.

You, Luke, Anakin, Palpatine, Yoda and Han Solo are involved in an insane tournament of Rock, Paper, Scissor, Lizard, Spock.

The catch here is that you are only allowed to use a fixed order of moves. If your order is "R", Then you have to use Rock, until you lose or win against everyone. If your order is RRV, then you have to use 2 Rocks followed by a Spock and keep repeating until you have won or lost.

Luke, Anakin, Palpatine, Yoda and Han Solo have submitted their respective orderings and you being an expert hacker got your hands on each of their orderings!

With this knowledge, you are to design your ordering for the tournament. Since everyone wants to win, you want to create an ordering such that you win the tournament by beating everyone. But this may not be possible under all circumstances.

Incase there is a possible winning order, print that out. If there is no possible way for you to win, print out -1 (or 0 or False or "impossible")

Input: a list of 5 orders

Output: a single order or -1

Sample Input 1

R
P
S
L
V

Sample Output 1

-1

Explanation 1

No matter what you play in your first move, there will be at least one person who beats you, hence it is not possible for you to win.

Sample Input 2

RPS
RPP
R
SRR
L

Sample Output 2

RPSP

Explanation 2

Once you play Rock in your first move, you end up beating "L" and "SRR" and tie against the rest. This is because Lizard and Scissors lose to Rock. When you play Paper next, you will beat "R" and tie against the remaining 2. This is because Rock loses to Paper. When you play Scissors next, you will win against "RPP" as Scissor beats Paper.

Finally, you will beat "RPS" with your Paper as Paper beats Rock.

Here are a list of notations (you may use any 5 literals, but please specify in your answer):

R : Rock
P : Paper
S : Scissor
L : Lizard
V : Spock

Here is a list of all possible outcomes:

winner('S', 'P') -> 'S'
winner('S', 'R') -> 'R'
winner('S', 'V') -> 'V'
winner('S', 'L') -> 'S'
winner('S', 'S') -> Tie
winner('P', 'R') -> 'P'
winner('P', 'V') -> 'P'
winner('P', 'L') -> 'L'
winner('P', 'S') -> 'S'
winner('P', 'P') -> Tie
winner('R', 'V') -> 'V'
winner('R', 'L') -> 'R'
winner('R', 'S') -> 'R'
winner('R', 'P') -> 'P'
winner('R', 'R') -> Tie
winner('L', 'R') -> 'R'
winner('L', 'V') -> 'L'
winner('L', 'S') -> 'S'
winner('L', 'P') -> 'L'
winner('L', 'L') -> Tie
winner('V', 'R') -> 'V'
winner('V', 'L') -> 'L'
winner('V', 'S') -> 'V'
winner('V', 'P') -> 'P'
winner('V', 'V') -> Tie

This is , so fewest bytes win.

P.S: Let me know if you need more test cases.

\$\endgroup\$
11
  • 5
    \$\begingroup\$ Please change "Star Trek" to "Star Wars" in your intro ;) \$\endgroup\$
    – movatica
    Commented May 5, 2019 at 14:05
  • 1
    \$\begingroup\$ This is a pretty difficult problem. Well, or I am bad at this kind of programming. \$\endgroup\$
    – CrabMan
    Commented May 5, 2019 at 14:44
  • \$\begingroup\$ @CrabMan This is a bit of a difficult problem to golf. especially in practical languages. \$\endgroup\$ Commented May 5, 2019 at 15:52
  • 1
    \$\begingroup\$ several works, but there are in theory infinite winning strategies, so keep that in mind \$\endgroup\$ Commented May 6, 2019 at 5:34
  • 1
    \$\begingroup\$ Related, and also a KOTH (cc: @Arnauld) \$\endgroup\$
    – DLosc
    Commented May 6, 2019 at 20:01

4 Answers 4

7
\$\begingroup\$

JavaScript (ES6),  122 115  112 bytes

Takes input as an array of strings of digits, with:

  • \$0\$ = Scissors (S)
  • \$1\$ = Paper (P)
  • \$2\$ = Rock (R)
  • \$3\$ = Lizard (L)
  • \$4\$ = Spock (V)

Returns either a string in the same format or \$false\$ if there's no solution.

f=(a,m='',x=0,o,b=a.filter(a=>(y=a[m.length%a.length])-x?o|=y-x&1^x<y:1))=>b+b?x<4&&f(a,m,x+1)||!o&&f(b,m+x):m+x

Try it online!

How?

This is a breadth-first search: we first try all moves at a given step to see if we can win the game. If we can't win right now, we try to add another move to each non-losing move.

The move identifiers were chosen in such a way that a move \$A\$ wins against a move \$B\$ if and only if \$(B-A)\bmod 5\$ is odd.

With \$A\$ on the left and \$B\$ on the top:

$$ \begin{array}{cc|ccccc} & & \text{(S)} & \text{(P)} & \text{(R)} & \text{(L)} & \text{(V)}\\ & & 0 & 1 & 2 & 3 & 4\\ \hline \text{(S) }&0 & - & \color{green}1 & \color{red}2 & \color{green}3 & \color{red}4\\ \text{(P) }&1 & \color{red}4 & - & \color{green}1 & \color{red}2 & \color{green}3\\ \text{(R) }&2 & \color{green}3 & \color{red}4 & - & \color{green}1 & \color{red}2\\ \text{(L) }&3 & \color{red}2 & \color{green}3 & \color{red}4 & - & \color{green}1\\ \text{(V) }&4 & \color{green}1 & \color{red}2 & \color{green}3 & \color{red}4 & - \end{array} $$

From there, we can deduce another way of testing if \$A\$ wins against \$B\$ for \$A\neq B\$:

((A - B) and 1) xor (B < A)

where and and xor are bitwise operators.

Commented

f = (                        // f is a recursive function taking:
  a,                         //   a[] = input
  m = '',                    //   m   = string representing the list of moves
  x = 0,                     //   x   = next move to try (0 to 4)
  o,                         //   o   = flag set if we lose, initially undefined
  b =                        //   b[] = array of remaining opponents after the move x
    a.filter(s =>            //     for each entry s in a[]:
    ( y =                    //       define y as ...
      s[m.length % s.length] //         ... the next move of the current opponent
    ) - x                    //       subtract x from y
    ?                        //       if the difference is not equal to 0:
      o |=                   //         update o using the formula described above:
        y - x & 1 ^ x < y    //           set it to 1 if we lose; opponents are removed
                             //           while o = 0, and kept as soon as o = 1
    :                        //       else (this is a draw):
      1                      //         keep this opponent, but leave o unchanged
  )                          //     end of filter()
) =>                         //
  b + b ?                    // if b[] is not empty:
    x < 4 &&                 //   if x is less than 4:
      f(a, m, x + 1)         //     do a recursive call with x + 1 (going breadth-first)
    ||                       //   if this fails:
      !o &&                  //     if o is not set:
        f(b, m + x)          //       keep this move and do a recursive call with b[]
  :                          // else (success):
    m + x                    //   return m + x
\$\endgroup\$
3
  • \$\begingroup\$ your code fails for the test case: test(['P','P','S','P','P']) The answer should be "SR" or "SV". \$\endgroup\$ Commented May 5, 2019 at 16:47
  • \$\begingroup\$ @KoishoreRoy Now fixed. \$\endgroup\$
    – Arnauld
    Commented May 5, 2019 at 18:14
  • 1
    \$\begingroup\$ This is actually a brilliant approach. I didn't even think of considering it as a graph. I was using dictionaries and reverse look-ups in my ungolfed original approach (without Spock or Lizards i.e.) \$\endgroup\$ Commented May 6, 2019 at 5:40
3
\$\begingroup\$

R, 213 190 bytes

-23 bytes thanks to Giuseppe.

function(L){m=matrix(rep(0:2,1:3),5,5)
m[1,4]=m[2,5]=1
v=combn(rep(1:5,n),n<-sum(lengths(L)))
v[,which(apply(v,2,function(z)all(sapply(L,function(x,y,r=m[cbind(x,y)])r[r>0][1]<2,z)))>0)[1]]}

Try it online!

If a solution exists, it outputs one. If there is no solution, it outputs a row of NA. If this output format is not acceptable, I can change it at a cost of a few bytes.

Moves are coded as 1=R, 2=S, 3=P, 4=L, 5=V, so that the matrix of outcomes is

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

(0=no winner; 1=player 1 wins; 2=player 2 wins)

An upper bound on the length of the solution if it exists is n=sum(lengths(L)) where L is the list of opponents' moves. The code creates all possible strategies of length n (stored in matrix v), tries all of them, and displays all winning strategies.

Note that this value of n makes the code very slow on TIO, so I have hardcoded in the TIO n=4 which is enough for the test cases.

For the first test case, the output is

     1 4 2 4

corresponding to the solution RLSL.

For the second test case, the output is

 NA NA NA NA

meaning that there is no solution.

Explanation of a previous version (will update when I can):

function(L){
  m = matrix(rep(0:2,1:3),5,5);
  m[1,4]=m[2,5]=1                      # create matrix of outcomes
  v=as.matrix(expand.grid(replicate(   # all possible strategies of length n
    n<-sum(lengths(L))                 # where n is the upper bound on solution length
    ,1:5,F)))             
  v[which(    
    apply(v,1,                         # for each strategy
          function(z)                  # check whether it wins
            all(                       # against all opponents
              sapply(L,function(x,y){  # function to simulate one game
                r=m[cbind(x,y)];       # vector of pair-wise outcomes
                r[r>0][1]<2            # keep the first non-draw outcome, and verify that it is a win
              }
              ,z)))
    >0),]                              # keep only winning strategies
}

The which is necessary to get rid of NAs which occur when the two players draw forever.

I am not convinced this is the most efficient strategy. Even if it is, I am sure that the code for m could be golfed quite a bit.

\$\endgroup\$
6
  • \$\begingroup\$ why is lengths() aliased to always return 4? \$\endgroup\$
    – Giuseppe
    Commented May 6, 2019 at 18:43
  • 1
    \$\begingroup\$ Anyway, while I'm waiting for your response, I golfed it down to 197, mostly by focusing on v... \$\endgroup\$
    – Giuseppe
    Commented May 6, 2019 at 18:45
  • \$\begingroup\$ @Giuseppe I have aliased lengths to force n=4 on TIO, because otherwise it takes too long to go over all \$5^n\$ (\$n=11\$) strategies. \$\endgroup\$ Commented May 6, 2019 at 19:08
  • \$\begingroup\$ ah, makes sense, should have known. 187 bytes \$\endgroup\$
    – Giuseppe
    Commented May 6, 2019 at 19:15
  • \$\begingroup\$ @Giuseppe Thanks, impressive golfing! I added 3 bytes to make the output more readable (otherwise we end up with the same solution(s) printed many times). \$\endgroup\$ Commented May 6, 2019 at 19:32
2
\$\begingroup\$

Jelly, 29 bytes

_%5ḟ0ḢḂ¬
ṁ€ZLḤƊçþ`Ạ€Tị;‘%5Ɗ$€

A monadic Link that accepts a list of lists of integers (each of which is an opponent's strategy) which yields a list of lists of integers - each of which is a winning strategy (so an empty list if none are possible).
(Just add to only yield a single strategy list or 0 if impossible.)

Try it online! (the footer formats to always show the lists)

Rock  Paper  Scissors  Spock  Lizard
0     1      2         3      4

Or try a letter mapped version (where strategies are taken and shown on their own lines using RPSVL notation).

How?

The numbers are chosen such that any which are an odd number greater than another modulo five win (i.e. they are numbered going around the edge of an inscribed pentagon of the throws).

The code plays each strategy off against every strategy (including themselves) for twice as many throws as the longest strategy so as to ensure finding any losers keeping those which are not defeated. The resulting list of strategies will contain a single strategy if there is an outright winner; no strategies if there was no winner; or multiple strategies if there are drawing players. After this a winning set of moves is appended to each of these strategies.

_%5ḟ0ḢḂ¬ - Link 1, does B survive?: list A, list B (A & B of equal lengths)
                              e.g. RPSR vs RPVL ->  [0,1,2,0], [0,1,3,4]
_        - subtract (vectorises)                    [0,0,-1,-4]
 %5      - modulo five (vectorises)                 [0,0,4,1]   ...if all zeros:
   ḟ0    - filter discard zeros (ties)              [4,1]                       []
     Ḣ   - head (zero if an empty list)             4                           0
      Ḃ  - modulo two                               0                           0
       ¬ - logical NOT                              1                           1

ṁ€ZLḤƊçþ`Ạ€Tị;‘%5Ɗ$€ - Main Link: list of lists of integers
ṁ€                   - mould each list like:
     Ɗ               -   last three links as a monad
  Z                  -     transpose
   L                 -     length
    Ḥ                -     double  (i.e. 2 * throws in longest strategy)
        `            - use left as both arguments of:
       þ             -   table using:
      ç              -     last Link (1) as a dyad
         Ạ€          - all for each (1 if survives against all others, else 0)
           T         - truthy indices
            ị        - index into the input strategies
                  $€ - last two links as a monad for each:
             ;       -   concatenate with:
                 Ɗ   -     last three links as a monad:
              ‘      -       increment (vectorises)
               %5    -       modulo five (vectorises)
\$\endgroup\$
4
  • \$\begingroup\$ I am completely new to Jelly, but it seems that you can gain a byte by replacing ZLḤ by . \$\endgroup\$ Commented May 7, 2019 at 11:54
  • \$\begingroup\$ @RobinRyder That won't work - it's only working with the example data because there are enough opponents and few enough throws - this is an example of one that wouldn't work. We need to analyse twice as many throws as the longest opponent strategy. (Your code is actually equivalent to this) \$\endgroup\$ Commented May 7, 2019 at 17:18
  • \$\begingroup\$ ...actually due to the action of Ɗ in your code it's not even doing what you may have thought - it's moulding each like it's own length then getting the cumulative sums of those results, so will also compare incorrect values too. Try this for example - it takes in [[1,2,3,4,5],[6,7],[8]], moulds each by the length of the entire list (3) to get [[1,2,3],[6,7,6],[8,8,8]] then performs accumulation to get [[1,1+2,1+2+3],[6,6+7,6+7+8],[8,8+8,8+8+8]] = [[1,3,6],[6,13,19],[8,16,24]]. \$\endgroup\$ Commented May 7, 2019 at 18:15
  • \$\begingroup\$ Ah yes, I knew I was misunderstanding something! \$\endgroup\$ Commented May 7, 2019 at 19:12
0
\$\begingroup\$

Emacs Lisp, 730 bytes

(require 'cl-extra)
(require 'seq)
(defun N (g) (length (nth 1 g)))
(defun M (g) (mapcar (lambda (o) (nth (% (N g) (length o)) o)) (car g)))
(defun B (x y) (or (eq (% (1+ x) 5) y) (eq (% (+ y 2) 5) x)))
(defun S (g) (seq-filter (lambda (m) (not (seq-some (lambda (v) (B v m)) (M g)))) '(0 1 2 3 4)))
(defun F (g) (cond ((null (car g)) (reverse (nth 1 g))) ((null (S g)) nil) ((>= (nth 3 g) (seq-reduce (lambda (x y) (calc-eval "lcm($,$$)" 'raw x y)) (mapcar 'length (car g)) 1)) nil) (t (cl-some (lambda (m) (F   (let ((r (seq-filter 'identity (mapcar* (lambda (v o) (and (not (B m v)) o)) (M g) (car g))))) (list r (cons m (nth 1 g)) (1+ (N g)) (if (eq (car g) r) (1+ (nth 3 g)) 0))))) (S g)))))
(defun Z (s) (F (list s () 0 0)))

I didn't find an online interpreter of Emacs Lisp :( If you have Emacs installed, you can copy code into a .el file, copy some testing lines below

;; 0 = rock, 1 = lizard; 2 = spock;
;; 3 = scissors; 4 = paper
(print (Z '((0) (1) (2) (3) (4))))
; output: nil
(print (Z '((0) (4) (3) (1))))
; output: nil
(print (Z '((0 4 3) (0 4 4) (0) (3 0 0) (1))))
; output: (0 4 3 0 1)
(print (Z '((4) (4) (3) (4) (4))))
; output: (3 0)
(print (Z '((4 3 2 1 0) (2 1 0 4 3))))
; output: (1)
(print (Z '((2) (2) (3) (0) (2) (3) (0) (0))))
; output: (2 1)
(print (Z '((2) (2 0) (3) (0) (2 1) (3) (0) (0))))
; output: nil

and run it $ emacs --script filename.el.

How it works

My program does depth first search with sometimes figuring out that it's impossible to win and terminating the branch it's on.

You can see full explanation in the unshortened version of the code:

(require 'seq)
(require 'cl-extra)

;; This program does depth first search with sometimes figuring out
;; that it's impossible to win and terminating the branch it's on.
;;

;; A move is a number from 0 to 4. 
;; https://d3qdvvkm3r2z1i.cloudfront.net/media/catalog/product/cache/1/image/1800x/6b9ffbf72458f4fd2d3cb995d92e8889/r/o/rockpaperscissorslizardspock_newthumb.png
;; this is a nice visualization of what beats what.
;; Rock = 0, lizard = 1, spock = 2, scissors = 3, paper = 4.

(defun beats (x y) "Calculates whether move x beats move y"
  (or (eq (% (1+ x) 5) y)
      (eq (% (+ y 2) 5) x)))

;; A gamestate is a list with the following elements:
(defun get-orders (gamestate)
  "A list of orders of players who haven't lost yet. Each order is a list of moves.
For example, ((2) (2 0) (3) (0) (2 1) (3) (0) (0)) is a valid orders list.
This function gets orders from the gamestate."
  (car gamestate))

;; At index 1 of the gamestate lies a list of all moves we have made so far in reverse order
;; (because lists are singly linked, we can't push back quickly)
(defun get-num-moves-done (gamestate)
  "Returns the number of moves the player has done so far"
  (length (nth 1 gamestate)))

(defun get-rounds-since-last-elim (gamestate)
  "The last element of a gamestate is the number of rounds passed since an opponent
was eliminated. We use this to determine if it's possible to win from current
gamestate (more about it later)."
  (nth 2 gamestate))

;; next go some utility functions
;; you can skip their descriptions, they are not very interesting
;; I suggest you skip until the next ;; comment

(defun get-next-move (order num-rounds-done)
  "Arguments: an order (e.g. (1 0 1)); how many rounds have passed total.
Returns the move this opponent will make next"
  (nth (% num-rounds-done (length order)) order))

(defun moves-of-opponents-this-round (gamestate)
  "Returns a list of moves the opponents will make next"
  (mapcar (lambda (order) (get-next-move order (get-num-moves-done gamestate)))
          (get-orders gamestate)))

(defun is-non-losing (move opponents-moves)
  "Calculates if we lose right away by playing move against opponents-moves"
  (not (seq-some (lambda (opponent-move) (beats opponent-move move))
                 opponents-moves)))

(defun non-losing-moves (gamestate)
  "Returns a list of moves which we can play without losing right away."
  (seq-filter
   (lambda (move) (is-non-losing move (moves-of-opponents-this-round gamestate)))
   '(0 1 2 3 4)))

(defun advance-gamestate (gamestate move)
  "If this move in this gamestate is non-losing, returns the next game state"
  (let ((new-orders (seq-filter
                    'identity (mapcar* (lambda (opp-move order)
                                         (and (not (beats move opp-move)) order))
                                       (moves-of-opponents-this-round gamestate)
                                       (get-orders gamestate)))))
  (list new-orders
        (cons move (nth 1 gamestate))
        (if (eq (get-orders gamestate) new-orders) (1+ (get-rounds-since-last-elim gamestate)) 0))))

;; How do we prevent our depth first search from continuing without halting?
;; Suppose 3 players (except us) are still in the game and they have orders of lengths a, b, c
;; In this situation, if least_common_multiple(a, b, c) rounds pass without an elimination
;; we will be in the same situation (because they will be playing the same moves they played
;; lcm(a, b, c) rounds ago)
;; Therefore, if it's possible to win from this gamestate,
;; then it's possible to win from that earlier game state,
;; hence we can stop exploring this branch

(defun get-cycle-len (gamestate)
  "Returns a number of rounds which is enough for the situation to become the same
if the game goes this long without an elimination."
  (seq-reduce (lambda (x y) (calc-eval "lcm($,$$)" 'raw x y))
              (mapcar 'length (get-orders gamestate)) 1))

(defun unwinnable-cycle (gamestate)
  "Using the aforementioned information, returns t if we are in such a
suboptimal course of play."
  (>= (get-rounds-since-last-elim gamestate) (get-cycle-len gamestate)))

(defun find-good-moves (gamestate)
  "Given gamestate, if it's possible to win
returns a list of moves, containing all moves already done + additional moves which lead to win.
Otherwise returns nil"
  (cond ((null (get-orders gamestate)) ; if no opponents left, we won, return the list of moves
         (reverse (nth 1 gamestate)))
        ((null (non-losing-moves gamestate)) ; if no non-losing moves available, this gamestate
         nil) ; doesn't lead to a win, return nil
        ((unwinnable-cycle gamestate) ; either it's impossible to win, or
         nil) ; it's possible to win from an earlier position, return nil
        (t (cl-some (lambda (move) ; otherwise return the first non-losing move which leads
                      (find-good-moves (advance-gamestate gamestate move))) ; to a non-nil result
                    (non-losing-moves gamestate)))))

(defun make-initial-gamestate (orders)
  "Given an orders list, create initial gamestate"
  (list orders () 0))
\$\endgroup\$
2
  • 1
    \$\begingroup\$ tio.run/##S81NTC7WzcksLvgPBAA can you insert your code here and try running it? \$\endgroup\$ Commented May 7, 2019 at 11:14
  • \$\begingroup\$ @KoishoreRoy I had tried tio.run and I couldn't figure out why it doesn't run. It says "Trailing garbage following expression" and I have no idea what that is and 5 minutes of googling didn't help me to fix it. \$\endgroup\$
    – CrabMan
    Commented May 8, 2019 at 13:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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