Using only vim to solve AdventOfCode Challenges | Episode 2

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

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

will translate to:

8
1
6

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

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']]

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

and our end goal is:

A X: score 
A Y: score
... 
C Z: score

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)

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'pA: =playerB + (playerA>1 && playerB>1 ? BonusOf(playerA,playerB) : BonusOf(playerA%3,playerB%3))jq

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

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

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}

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}

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

let next = {'rock': 'scissors', 'paper': 'rock', 'scissors': 'paper'}

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")

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, ' ')")

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

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]]})

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])]})

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]")})

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})

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

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]}

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}

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]]}

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})

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'])))
Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
sql-to-python-pandas:-a-sql-user’s-quick-guide

SQL to Python Pandas: A SQL User’s Quick Guide

Next Post
#bitcoin-fee-hits-$37

#Bitcoin fee hits $37👀

Related Posts