DEV Community

Cover image for Using only vim to solve AdventOfCode Challenges | Episode 2
Cipherlogs
Cipherlogs

Posted on • Originally published at cipherlogs.com

Using only vim to solve AdventOfCode Challenges | Episode 2

Note: It's important to read the first episode to understand the context of how these challenges are solved.

1. Getting Started

Open today's challenge and make sure that you are in sync with your team, if you don't have a team yet submit your application here

Take your time with the challenge and solve the puzzle in the two mentioned scenarios. Good luck.

1.1 Rules

  1. In the first scenario, if you plan to hardcode all possible outcomes of the game, avoid manual entry. Instead, find an automatic way to handle them. Imagine having one million potential outcomes for the game—your approach should be automated for speed. Utilize your editor for efficiency. Remember, the emphasis in the first scenario is on speed.

  2. In the second scenario, write a Vimscript that accommodates the worst-case scenario. Consider a situation where each symbol might signify something else in the future. Structure your code to distinctly separate the game logic from symbol deciphering.

1.2 Sample Data

The provided sample data should be used for testing the commands. Once you feel comfortable and confident, you can apply these commands to your actual input data.

Input:

A Y
B X
C Z
Enter fullscreen mode Exit fullscreen mode

Expected output:

  • part1: 15
  • part2: 12

2. Solutions

This guide assumes you've already watched the accompanying walk-through YouTube video. If you haven't, please do.

2.1 The First Scenario

2.1.1 Part One

Our input data consists of symbols representing each player's move, and our task is to calculate the score of each game. The quickest way is to map all possible outcomes and replace each line with the corresponding score. For example:

A Y
B X
C Z
Enter fullscreen mode Exit fullscreen mode

will translate to:

8
1
6
Enter fullscreen mode Exit fullscreen mode

This mapping can be done using Vim dictionaries, acting like hashtables or objects. Below is a function Swap containing a dictionary with all possible game outcomes:

function! Swap(line)
  let scores = {
    \ 'A X': '4',
    \ 'A Y': '8',
    \ 'A Z': '3',
    \ 'B X': '1',
    \ 'B Y': '5',
    \ 'B Z': '9',
    \ 'C X': '7',
    \ 'C Y': '2',
    \ 'C Z': '6',
  \ }

  return scores[a:line]
endfunction
Enter fullscreen mode Exit fullscreen mode

While we can use this function immediately, today's challenge rules prohibit hard coding outcomes. Instead, we need a fast and creative way to enter them.

To generate all possible symbols, use :r! echo \\n{A..C}" "{X..Z}.

In the walkthrough video, alternatives to Bash were discussed, showcasing the flexibility to choose more powerful tools based on specific needs.

This Haskell code achieves the same result as the previous Bash code, but with a key advantage: scalability. We can easily make further transformations to the generated output by adding additional functions to the pipeline.

main = putStr $ unlines [[x, ' ', y] | x <- ['A'..'C'], y <- ['X'..'Z']]
Enter fullscreen mode Exit fullscreen mode

So far, this is what we have:

A X 
A Y 
A Z 
B X 
B Y 
B Z 
C X 
C Y 
C Z
Enter fullscreen mode Exit fullscreen mode

and our end goal is:

A X: score 
A Y: score
... 
C Z: score
Enter fullscreen mode Exit fullscreen mode

First let's create a function on the fly, that will assign the bonus points, if we loose we get no bonus, if we end with a draw we get the hand move point plus 3 and if we win we get the hand move point plus 6.

let BonusOf = {a, b -> a > b ? 0 : (a < b ? 6 : 3)
Enter fullscreen mode Exit fullscreen mode

Now, to assign the correct score to each combination, let's record a quick macro.

Start by recording a macro on the first line with the following commands:

:let ln = line('.')
:let playerA = ln-1/3 + 1
:let playerB = ln%3 ? ln%3 : 3

gg0C'p<Esc>A: <C-r>=playerB + (playerA>1 && playerB>1 ? BonusOf(playerA,playerB) : BonusOf(playerA%3,playerB%3))jq
Enter fullscreen mode Exit fullscreen mode

As we covered in the walkthrough video, we're using line numbers to determine the value of each hand. By default, "A, X" is 1, "B, Y" is 2, and so on.

We also use BonusOf(playerA%3, playerB%3) to round the value of each hand by three. This is because "Rock" is typically 1 and "Paper" is 2. However, we need to adjust this logic for "Scissors" (3), which should not be considered stronger than "Rock." The walkthrough video explains this rounding process in more detail.

Now, let's apply the macro we recorded on the first line to the rest of the lines. Simply use .,$ norm @q and you're good to go!

Then wrap everything in a function:

function! Swap(line)
  let scores = {
        \ 'A X': 4,
        \ 'A Y': 8,
        \ 'A Z': 3,
        \ 'B X': 1,
        \ 'B Y': 5,
        \ 'B Z': 9,
        \ 'C X': 7,
        \ 'C Y': 2,
        \ 'C Z': 6,
        \ }

  return scores[a:line]
endfunction
Enter fullscreen mode Exit fullscreen mode

Open the input file and run the function on each line using :%s/.*/\=Swap(submatch(0)). To calculate the sum of all lines, use %! paste -sd+ | bc.

2.1.2 Part Two

For the second part of the challenge, the approach remains similar. Adjust the way you calculate the score for each round. You'll end up with a function like this:

function! Swap2(line)
  let scores = {
    \ 'A X': 3,
    \ 'A Y': 4,
    \ 'A Z': 8,
    \ 'B X': 1,
    \ 'B Y': 5,
    \ 'B Z': 9,
    \ 'C X': 2,
    \ 'C Y': 6,
    \ 'C Z': 7,
  \ }

  return scores[a:line]
endfunction
Enter fullscreen mode Exit fullscreen mode

Apply the same steps as in Part One to obtain the answer for the second part efficiently. Don't retype your previous commands, just recall them from the command history. this step shouldn't take more than 5 seconds.

2.2 The Second Scenario

Defining Essential Functions

First and foremost, let's address the essential functions required for our game logic:

let Bonus = {a, b -> a > b ? 0 : (a < b ? 6 : 3)}
let Score = {a, b -> a>1 && b>1 ? g:Bonus(a, b) + b : g:Bonus(a%3, b%3) + b}
Enter fullscreen mode Exit fullscreen mode

These functions establish the scoring mechanism for the game.

Handling Hand Moves

To proceed, we need to assign points to each hand move:

let pointOf = {'rock': 1, 'paper': 2, 'scissors': 3}
Enter fullscreen mode Exit fullscreen mode

Additionally, we create a dictionary to map each move to its defeating move:

let next = {'rock': 'scissors', 'paper': 'rock', 'scissors': 'paper'}
Enter fullscreen mode Exit fullscreen mode

Processing Input Data

Assuming our input data is stored in a file named input, we start by reading and printing its content:

echo readfile("input")
Enter fullscreen mode Exit fullscreen mode

The output is a list of lines in the format ['symbolA symbolB', 'symbolA symbolB', ...]. We transform it to [['symbolA', 'symbolB'], ...]:

echo
      \ readfile("inputdemo.vim")
      \ ->map("split(v:val, ' ')")
Enter fullscreen mode Exit fullscreen mode

The next step now is to translate each symbol to what it mean, whether is it 'rock', 'paper' or 'scissors'. for now, we are still in the testing phase, let's create a dummy function that will do this translation.

function ToHand(symbol)
  return {_ -> 'rock'}
endfunction
Enter fullscreen mode Exit fullscreen mode

Our ToHand function takes a symbol and returns a new function. This new function, in turn, takes an argument and ultimately returns the name of the hand. In our example, it will always return "rock."

You might wonder why we don't just create a function that directly returns the string "rock" instead of adding an extra layer of abstraction. The reason is that the function we return will later be able to accept symbols from either the left or right column. This allows each function to have information about the opponent's move.

While ToHand may seem simple now, we'll revisit it and expand its functionality later.

echo
      \ readfile("inputdemo.vim")
      \ ->map("split(v:val, ' ')")
      \ ->map({_, xs -> [ToHand(xs[0])(xs[1]), xs[1]]})
Enter fullscreen mode Exit fullscreen mode

Here, we map over each element, applying ToHand to symbols in the left column for player one's move, and then applying it to the right column for player two's move.

      \ ->map({_, xs -> [xs[0], ToHand(xs[1])(xs[0])]})
Enter fullscreen mode Exit fullscreen mode

While applying both functions simultaneously might seem tempting, it would make our lambda function unnecessarily complex. Instead, we'll follow the natural flow of the game: player one plays their move, the game state updates, and then player two reacts based on the updated state. This approach keeps our code easy to read.

Next, we convert hand names to their corresponding points:

      \ ->map({_, xs -> map(xs, "g:pointOf[v:val]")})
Enter fullscreen mode Exit fullscreen mode

We call the Score function to obtain the score for each game round and sum up all the numbers in the list:

      \ ->map({_, v -> g:Score(v[0], v[1])})
      \ ->reduce({a, b -> a + b})
Enter fullscreen mode Exit fullscreen mode

This encapsulates the entire game logic.

Note: While this script utilizes nested maps, it aligns with the nature of Vimscript. In other languages, we might explore composition or transduction, but for now, we'll keep Vimscript within its natural confines. Any future modifications will be limited to the ToHand function.

Note: I have written this using map on top of map. for this script is fine, in other languages, I might compose or transduce but let's not push vimscript doing things that aren't naturally built for. However, it's acknowledged that in future iterations, Lua may be explored for enhanced functionality, as hinted in the seond season.

Moving forward, the sole modification we'll make is to our placeholder function ToHand; all other aspects of the script remain unchanged.

2.2.1 Part One

In the initial phase of the puzzle, we assign 'rock' to A and X, 'paper' to B and Y, and 'scissors' to C and Z.

Now, let's refine our 'ToHand' function to accurately represent each symbol.

function ToHand(symbol)
  let symbols = {
        \ 'A': {_ -> 'rock'},
        \ 'B': {_ -> 'paper'},
        \ 'C': {_ -> 'scissors'},
        \ 'X': {_ -> 'rock'},
        \ 'Y': {_ -> 'paper'},
        \ 'Z': {_ -> 'scissors'},
        \}

  return symbols[a:symbol]
endfunction
Enter fullscreen mode Exit fullscreen mode

This modification ensures that our 'ToHand' function correctly translates each symbol to its corresponding hand gesture.

And that's it – we're finished!

2.2.2 Part Two

In the second part, we define the conditions for three scenarios: losing, drawing, and winning denoted by X, Y, and Z respectively. Achieving these outcomes involves specific modifications to our ToHand function.

        \ 'X': {x -> g:next[x]}
Enter fullscreen mode Exit fullscreen mode

For scenario X, which corresponds to a loss, we take the enemy's hand move, represented by arg x, and call the next function to determine the subsequent move required for a loss.

        \ 'Y': {x -> x}
Enter fullscreen mode Exit fullscreen mode

In the case of Y, signifying a draw, we simply return the same enemy move denoted by x.

        \ 'Z': {x -> g:next[g:next[x]]}
Enter fullscreen mode Exit fullscreen mode

For Z, indicating a win, we perform the inverse of the X scenario.

2.2.3 The Final Script

That's all the necessary adjustments to the ToHand function. The final and comprehensive script is provided below.

function ToHand(symbol)
  let symbols = {
        \ 'A': {_ -> 'rock'},
        \ 'B': {_ -> 'paper'},
        \ 'C': {_ -> 'scissors'},
        \ 'X': {x -> g:next[x]},
        \ 'Y': {x -> x},
        \ 'Z': {x -> g:next[g:next[x]]},
        \}

  return symbols[a:symbol]
endfunction


" CORE, DO NOT TOUCH
let pointOf = {'rock': 1, 'paper': 2, 'scissors': 3}
let next = {'rock': 'scissors', 'paper': 'rock', 'scissors': 'paper'}
let Bonus = {a, b -> a > b ? 0 : (a < b ? 6 : 3)}
let Score = {a, b -> a>1 && b>1 ? g:Bonus(a, b) + b : g:Bonus(a%3, b%3) + b}

echo
      \ readfile("inputdemo.vim")
      \ ->map("split(v:val, ' ')")
      \ ->map({_, xs -> [ToHand(xs[0])(xs[1]), xs[1]]})
      \ ->map({_, xs -> [xs[0], ToHand(xs[1])(xs[0])]})
      \ ->map({_, xs -> map(xs, "g:pointOf[v:val]")})
      \ ->map({_, v -> g:Score(v[0], v[1])})
      \ ->reduce({a, b -> a + b})
Enter fullscreen mode Exit fullscreen mode

Exercises

  1. In your preferred programming language, create a script that can automatically generate all potential game outcomes and their score.

  2. Run your code in Vim to generate all the required data automatically.

Below is an example using Javascript:

const log = console.log;

const getCombos = xs => ys =>
  xs.flatMap (x => ys.map (y => [x, y]));

const pointOf = move => {
  const moves = {'X': 1, 'Y': 2, 'Z': 3,};
  return moves[move];
};

const some = xs => xA => xB =>
  xs
    .map (x => x.split(""))
    .some (x => x[0] === xA && x[1] === xB);

const isDraw = some (['AX', 'BY', 'CZ']);

const isLost = some (['AZ', 'BX', 'CY']);

const format = combos => {
  return (
    combos.map (combo => {
      const enemyMove = combo[0];
      const myMove = combo[1];
      const shapeScore = pointOf (myMove);

      const score = (
        isDraw (enemyMove) (myMove)
          ? 3 + shapeScore
          : isLost (enemyMove) (myMove) ? shapeScore : 6 + shapeScore
      );

      return `'${enemyMove} ${myMove}':${score},`
    })
  );
};

log (format (getCombos (['A', 'B', 'C']) (['X', 'Y', 'Z'])))
Enter fullscreen mode Exit fullscreen mode

Top comments (0)