Enigmatic Code

Programming Enigma Puzzles

Tantalizer 448: Love and hate

From New Scientist #999, 6th May 1976

To play this vaguely suggestive game you will need nine cards printed with the words:

LOVE AND HATE FEY GUY AGO TONY GUILT NIL.

You and your opponent take it in turns to pick a word until one of you has collected three with a letter in common. Whoever does so first is the winner.

You lose the toss and your opponent snaps up LOVE. Assuming him to be a master at the game, what must you take to stop him winning?

[tantalizer448]

5 responses to “Tantalizer 448: Love and hate

  1. Jim Randell 25 July 2018 at 8:56 am

    This Python 3 program plays the game with appropriate strategies for the players.

    Player 1 is a perfect player, and will prefer to make a play that guarantees him a win. If that is not possible he will try to make a play that guarantees the opponent cannot win.

    Player 2’s strategy is to try and make a play that prevents Player 1 from winning.

    The program explores all possible moves that conform to the above strategies, but we could simplify it so that the strategies determine a single “best” play in any situation, and not to track the actual sequence of moves.

    As it is, this program executes in 755ms.

    Run: [ @repl.it ]

    from itertools import combinations
    from collections import defaultdict
    from enigma import join, printf
    
    # the collection of words
    words = 'LOVE AND HATE FEY GUY AGO TONY GUILT NIL'.split()
    
    # find sets of 3 words with a common letter
    wins = list(set((x, y, z)) for (x, y, z) in combinations(words, 3) if set(x).intersection(y, z))
    
    # is sequence <s> a winning sequence?
    won = lambda s: any(w.issubset(s) for w in wins)
    
    # how we designate outcomes
    outcome = {
      '1': 'player 1 wins',
      '2': 'player 2 wins',
      'X': 'draw',
    }
    
    # select keys from a dictionary where the value satisfy the specified function
    def select(fn=lambda x: True):
      return (lambda d: (k for (k, v) in d.items() if fn(v)))
    
    # apply strategy functions in order
    def strategy(d, fns=[]):
    
      # play all possible outcomes if no functions work
      fns.append(select())
    
      # try each function in turn
      sat = 0
      for fn in fns:
        # find plays that satisfy this function
        for k in fn(d):
          # and play them
          yield from d[k]
          sat = 1
        if sat: break
    
    # strategy for first player
    def player1(d):
      return strategy(d, [
        # are there choices that ensure a player 1 win?
        select(lambda v: all(w == '1' for (w, s) in v)),
        # are there choices that exclude a player 2 win?
        select(lambda v: all(w != '2' for (w, s) in v)),
      ])
    
    # strategy for second player
    def player2(d):
      return strategy(d, [
        # are there choices that exclude a player 1 win?
        select(lambda v: all(w != '1' for (w, s) in v)),
      ])
    
    
    # play the game
    def play(s, ws):
      # p = 0: player 2 has just played, it's player 1's go
      # p = 1: player 1 has just played, it's player 2's go
      p = (len(s) % 2)
      if p == 1 and len(s) > 4 and won(s[0::2]):
        # first player wins
        yield ('1', s)
      elif p == 0 and len(s) > 5 and won(s[1::2]):
        # second player wins
        yield ('2', s)
      elif not ws:
        # draw
        yield ('X', s)
      else:
        # collect outcomes for each word
        d = dict((w, list(play(s + [w], ws[:i] + ws[i + 1:]))) for (i, w) in enumerate(ws))
        # choose words to play according to the appropriate strategy
        fn = (player1, player2)[p]
        yield from fn(d)
    
    # play the game and collect outcomes by player 2's choice
    r = defaultdict(lambda: defaultdict(int))
    for (w, s) in play(words[:1], words[1:]):
      printf("[{w} <- ({s})]", w=outcome[w], s=join(s, sep=" "))
      r[s[1]][w] += 1
    
    # output choices for player 2
    for (k, d) in r.items():
      printf("play {k}:")
      # output outcomes for this choice
      for (w, n) in d.items():
        printf("  {w}: {n} games", w=outcome[w])
    

    Solution: Player 2 must play TONY in order to stop Player 1 from winning.

    There are 8 sets of 3 words that share a letter, and if we place the words on a grid, as below, and draw lines connecting the winning sets, we get:

    We see that the game is equivalent to Noughts and Crosses (see [ https://en.wikipedia.org/wiki/Tic-tac-toe ]).

    In the situation described in the puzzle, Player 1 chooses a corner square (LOVE), so the only situation in which Player 2 can force a draw (if Player 1 is a perfect player) is by playing the centre square (TONY).

    If Player 2 plays an edge square next to Player 1 (say NIL, but playing FEY will play out similarly) then Player 1 will respond by playing the centre square (TONY). Player 2 must then play AGO in order to stop Player 1 from winning on the next move, and Player 1 can then play HATE to acquire 2 of the E words and 2 of the T words. Player 2 cannot stop Player 1 from acquiring one of the remaining words required to make a line, so always loses in this scenario.

    If Player 2 plays one of the other edge squares (say AND, but GUY will play out similarly) the Player 1 responds with the centre square (TONY). Player 2 must play AGO, and Player 1 plays HATE, giving him the option of FEY and GUILT to win on E or T, and Player 2 cannot block both these. So Player 2 always loses in this scenario too.

    If Player 2 played one of the remaining corner squares, then Player 1 can respond by playing a corner square and can force a win.

    So the only scenario in which Player 2 can do better is to play the centre square (TONY) and force a draw.

    As the program shows there are 82 possible games that start with LOVE and TONY and all end in a draw.

  2. Jim Olson 25 July 2018 at 10:45 pm

    Doesn’t Player 1 win with Love,Tony by playing the corners Guilt then Hate and finally Ago.

    • Jim Randell 25 July 2018 at 11:17 pm

      Player 1 can’t win by playing LOVE, GUILT, HATE, AGO, because no 3-subset of those 4 words have a letter in common.

      Also, if the game sequence was 1=LOVE, 2=TONY, 1=GUILT, then Player 2 would be forced to play NIL (to stop Player 1 from completing the L line). If Player 1 then played one of the remaining corners (HATE or AGO), Player 2 could play AND to win along the N line.

      • Jim Randell 26 July 2018 at 8:08 am

        Or are you suggesting that if the game opens with Player 1 taking a corner, and Player 2 takes the centre square, then if Player 1 can acquire the remaining three corners, on his final go he will be guaranteed to make a line by playing the last edge square?

        The flaw in this scheme is that while Player 1 is acquiring the corners, Player 2 is collecting the edge squares, and by the time he has the centre square and 2 or 3 edge squares he will have collected a line and won, so Player 1 will never get to make his final move.

        • Jim Olson 26 July 2018 at 5:56 pm

          Yes, the latter but you immediately demonstrated my error of not considering the obvious rules of the game. So I had the correct answer after all and was talking myself out of it.

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: