---

# Hint


This Kata is an extension of the earlier ones in this series. Completing those first will make this task easier.

# Background

My TV remote control has arrow buttons and an `OK` button.

I can use these to move a "cursor" on a logical screen keyboard to type words...

# Keyboard

The screen "keyboard" layouts look like this


  #tvkb {
    width : 400px;
    border: 5px solid gray; border-collapse: collapse;
  }
  #tvkb td {
    color : orange;
    background-color : black;
    text-align : center;
    border: 3px solid gray; border-collapse: collapse;
  }
  #legend {
    width : 400px;
    border: 1px solid gray; border-collapse: collapse;
  }
  #legend td {
    text-align : center;
    border: 1px solid gray; border-collapse: collapse;
  }



Keypad Mode 1 = alpha-numeric (lowercase)
Keypad Mode 3 = symbols




abcde123
fghij456
klmno789
pqrst.@0
uvwxyz_/
aA#SP




^~?!'"()
-:;+&%*=
<>€£$¥¤\
[]{},.@§
#¿¡_/
aA#SP






* `aA#` is the SHIFT key. Pressing this key cycles through THREE keypad modes.
 * **Mode 1** = alpha-numeric keypad with lowercase alpha (as depicted above)
 * **Mode 2** = alpha-numeric keypad with UPPERCASE alpha
 * **Mode 3** = symbolic keypad (as depicted above)
 
 
* `SP` is the space character
* The other (solid fill) keys in the bottom row have no function

## Special Symbols

For your convenience, here are Unicode values for the less obvious symbols of the **Mode 3** keypad


¡ = U-00A1£ = U-00A3¤ = U-00A4¥ = U-00A5
§ = U-00A7¿ = U-00BF€ = U-20AC


# Kata task

How many button presses on my remote are required to type the given `words`?

## Notes

* The cursor always starts on the letter `a` (top left)
* The inital keypad layout is **Mode 1**
* Remember to also press `OK` to "accept" each letter
* Take the shortest route from one letter to the next
* The cursor wraps, so as it moves off one edge it will reappear on the opposite edge
* Although the blank keys have no function, you may navigate through them if you want to
* Spaces may occur anywhere in the `words` string
* Do not press the SHIFT key until you need to. For example, with the word `e.Z`, the SHIFT change happens **after** the `.` is pressed (not before). In other words, do not try to optimize total key presses by pressing SHIFT early.

```if:c,cpp
## C/C++ warriors
The standard I/O libraries choke on wide characters beyond the value 255. This kata includes the Euro € (U-20AC). So the function `ws2utf8()` has been preloaded for converting wchar_t strings to UTF-8 for printing.
```

# Example

words = `Too Easy?`

* T => `a`-`aA#`-OK-`U`-`V`-`W`-`X`-`Y`-`T`-OK = 9
* o => `T`-`Y`-`X`-`W`-`V`-`U`-`aA#`-OK-OK-`a`-`b`-`c`-`d`-`e`-`j`-`o`-OK = 16
* o => `o`-OK = 1
* space => `o`-`n`-`m`-`l`-`q`-`v`-`SP`-OK = 7
* E => `SP`-`aA#`-OK-`A`-`3`-`2`-`1`-`-E`-OK = 8
* a => `E`-`1`-`2`-`3`-`A`-`aA`-OK-OK-`a`-OK = 9
* s => `a`-`b`-`c`-`d`-`i`-`n`-`s`-OK = 7
* y => `s`-`x`-`y`-OK = 3
* ? => `y`-`x`-`w`-`v`-`u`-`aA#`-OK-OK-`^`-`~`-`?`-OK = 11

Answer = 9 + 16 + 1 + 7 + 8 + 9 + 7 + 3 + 11 = 71



*Good Luck!
DM.*



Series
* TV Remote
* TV Remote (shift and space)
* TV Remote (wrap)
* TV Remote (symbols)

Difficulty: introductory

Starter code:

def tv_remote(words):
	

Solutions:
H, W     = 6, 8
KEYBOARD = [ "abcde123fghij456klmno789pqrst.@0uvwxyz_/\u000e ",
             "ABCDE123FGHIJ456KLMNO789PQRST.@0UVWXYZ_/\u000e ",
             "^~?!'\"()-:;+&%*=<>€£$¥¤\\[]{},.@§#¿¡\u000e\u000e\u000e_/\u000e "]
MAP      = [ {c: (i//W, i%W) for i,c in enumerate(KEYBOARD[x])} for x in range(len(KEYBOARD)) ]


def manhattan(*pts):
    dxy = [abs(z2-z1) for z1,z2 in zip(*pts)]
    return 1 + sum( min(dz, Z-dz) for dz,Z in zip(dxy, (H,W)) )

def tv_remote(words):
    cnt, mod, was = 0, 0, 'a'
    for c in words:
        while c not in KEYBOARD[mod]:
            cnt += manhattan(MAP[mod][was], MAP[mod]['\u000e'])
            was = '\u000e'
            mod = (mod+1) % 3
        cnt += manhattan(MAP[mod][was], MAP[mod][c])
        was  = c
    return cnt

M1 = "abcde123fghij456klmno789pqrst.@0uvwxyz_/° "
M2 = "ABCDE123FGHIJ456KLMNO789PQRST.@0UVWXYZ_/° "
M3 = "^~?!\'\"()-:;+&%*=<>€£$¥¤\\[]{},.@§#¿¡°°°_/° "

D1 = {c:divmod(i, 8) for i,c in enumerate(M1)}
D2 = {c:divmod(i, 8) for i,c in enumerate(M2)}
D3 = {c:divmod(i, 8) for i,c in enumerate(M3)}
D = (D1, D2, D3)

dist = lambda i,j,k,l: min(abs(i-k), 6-abs(i-k)) + min(abs(j-l), 8-abs(j-l)) + 1
    
def tv_remote(words):
    def rec(c, d, i, j):
        if c == len(words): return 0
        tmp = D[d].get(words[c])
        if tmp: return dist(i, j, *tmp) + rec(c+1, d, *tmp)
        return dist(i, j, 5, 0) + rec(c, (d+1)%3, 5, 0)
    return rec(0, 0, 0, 0)

from collections import namedtuple
from itertools import repeat, cycle

Position = namedtuple('Position', 'y x')
shift = Position(5, 0)

remote = (
    (
        'a', 'b', 'c', 'd', 'e', '1', '2', '3',
        'f', 'g', 'h', 'i', 'j', '4', '5', '6',
        'k', 'l', 'm', 'n', 'o', '7', '8', '9',
        'p', 'q', 'r', 's', 't', '.', '@', '0',
        'u', 'v', 'w', 'x', 'y', 'z', '_', '/',
        '', ' '
    ),
    (
        'A', 'B', 'C', 'D', 'E', '1', '2', '3',
        'F', 'G', 'H', 'I', 'J', '4', '5', '6',
        'K', 'L', 'M', 'N', 'O', '7', '8', '9',
        'P', 'Q', 'R', 'S', 'T', '.', '@', '0',
        'U', 'V', 'W', 'X', 'Y', 'Z', '_', '/',
        '', ' '
    ),
    (
        '^', '~', '?', '!', "'", '"', '(', ')',
        '-', ':', ';', '+', '&', '%', '*', '=',
        '<', '>', '€', '£', '$', '¥', '¤', '\\',
        '[', ']', '{', '}', ',', '.', '@', '§',
        '#', '¿', '¡', '', '', '', '_', '/',
        '', ' '
    )
)


def tv_remote(word: str):
    modes = cycle(remote)
    prev, mode, button_presses = Position(0, 0), next(modes), 0

    for letter in word:
        try:
            i = mode.index(letter)

        except ValueError:
            button_presses += calc_presses(prev, shift)
            mode = next(modes)
            prev = shift

            try:
                i = mode.index(letter)

            except ValueError:
                button_presses += 1
                mode = next(modes)
                i = mode.index(letter)

        cur = Position(*divmod(i, 8))
        button_presses += calc_presses(prev, cur)
        prev = cur

    return button_presses


def calc_presses(pos1: Position, pos2: Position):
    dif_y, dif_x = abs(pos1.y - pos2.y), abs(pos1.x - pos2.x)
    return min(dif_y, 6 - dif_y) + min(dif_x, 8 - dif_x) + 1

def tv_remote(word):
    shift, kb = coords["↑"], 0
    moves, current = 0, (0, 0)
    for char in word:
        target, switch = coords[char.lower()], get_switch(kb, char)
        if switch:
            moves, current, kb = moves + switch-1 + distance(current, shift), shift, (kb + switch) % 3
        moves, current = moves + distance(current, target), target
    return moves

kb1 = ("abcde123", "fghij456", "klmno789", "pqrst.@0", "uvwxyz_/", "↑ ")
kb3 = ("^~?!'\"()", "-:;+&%*=", "<>€£$¥¤\\", "[]{},.@§", "#¿¡   _/")    
coords = {c: (line.index(c), y) for kb in (kb1, kb3) for y, line in enumerate(kb) for c in line}

def get_switch(kb, char):
    target = (0 if char.islower() else 1 if char.isupper()
              else (kb != 2 and kb) if char.isdecimal() else 2 if char not in ".@_/↑ " else kb)
    return (target - kb) % 3

def distance(pos1, pos2):
    d0, d1 = abs(pos2[0] - pos1[0]), abs(pos2[1] - pos1[1])
    return 1 + min(d0, 8 - d0) + min(d1, 6 - d1)

def tv_remote(words):
    kpad=['abcde123fghij456klmno789pqrst.@0uvwxyz_/○ ☺☺☺☺☺',
    'ABCDE123FGHIJ456KLMNO789PQRST.@0UVWXYZ_/○ ☺☺☺☺☺☺',
    '^~?!\'"()-:;+&%*=<>€£$¥¤\\[]{},.@§#¿¡☺☺☺_/○ ☺☺☺☺☺☺']
    lenght, coord = [0 for _ in range(2)]
    lst=list(kpad[0])
    for i in words:
        while kpad[0].find(i)==-1:
            lenght,coord = calculate(lst,'○',lenght,coord)
            kpad=kpad[-2:]+kpad[:-2]
            lst=list(kpad[0])
        lenght,coord = calculate(lst,i,lenght,coord)
    return lenght

def calculate(lst, i, lenght, coord):
    x=abs(lst.index(i)%8-coord%8)
    y=abs(lst.index(i)//8-coord//8)
    lenght += min(x,8-x)+min(y,6-y)+1
    return [lenght,lst.index(i)]

KEYPADS = (
    (
        'a', 'b', 'c', 'd', 'e', '1', '2', '3',
        'f', 'g', 'h', 'i', 'j', '4', '5', '6',
        'k', 'l', 'm', 'n', 'o', '7', '8', '9',
        'p', 'q', 'r', 's', 't', '.', '@', '0',
        'u', 'v', 'w', 'x', 'y', 'z', '_', '/',
        'aA#', 'SP', ' ', ' ', ' ', ' ', ' ', ' ',
    ),
    (
        'A', 'B', 'C', 'D', 'E', '1', '2', '3',
        'F', 'G', 'H', 'I', 'J', '4', '5', '6',
        'K', 'L', 'M', 'N', 'O', '7', '8', '9',
        'P', 'Q', 'R', 'S', 'T', '.', '@', '0',
        'U', 'V', 'W', 'X', 'Y', 'Z', '_', '/',
        'aA#', 'SP', ' ', ' ', ' ', ' ', ' ', ' ',
    ),
    (
        '^', '~', '?', '!', '\'', '"', '(', ')',
        '-', ':', ';', '+', '&', '%', '*', '=',
        '<', '>', '€', '£', '$', '¥', '¤', '\\',
        '[', ']', '{', '}', ',', '.', '@', '§',
        '#', '¿', '¡', ' ', ' ', ' ', '_', '/',
        'aA#', 'SP', ' ', ' ', ' ', ' ', ' ', ' ',
    )
)

class Tv:
    def __init__(self, words):
        self.words = words
        self.OK = 1
        self.MAX_H = 7
        self.MAX_V = 5
        self.count = 0
        self.keypad_pos = 0
        self.current_pos = (0, 0)
        self.CHANGE_KEY_IDX = 40
        

    def count_best_path(self, index: int):

        # coordinates of the letter
        temp_pos = ((index // 8), (index % 8))

        result = []

        # finding the smallest path movements
        for current_val, temp_val, max_l in zip(self.current_pos, temp_pos, [self.MAX_V, self.MAX_H]):
            v_1 = current_val
            v_2 = current_val
            count = 0

            while v_1 != temp_val and v_2 != temp_val:
                v_1 -= 1
                v_2 += 1
                if v_1 < 0:
                    v_1 = max_l
                if v_2 > max_l:
                    v_2 = 0
                count += 1

            result.append(count)

        # update the current position
        self.current_pos = temp_pos

        # update the count
        self.count += sum(result) + self.OK


    def count_total_moves(self):
        for letter in self.words:

            letter = 'SP' if letter == ' ' else letter
            while letter not in KEYPADS[self.keypad_pos]:
                self.count_best_path(self.CHANGE_KEY_IDX)
                if self.keypad_pos < 2:
                    self.keypad_pos += 1
                else:
                    self.keypad_pos = 0

            index = KEYPADS[self.keypad_pos].index(letter)
            self.count_best_path(index)
        return self.count

def tv_remote(words):    
    return Tv(words).count_total_moves()

print(tv_remote('ciao'))

def tv_remote(words):
    page1 = {'a':[0,0],'b':[0,1],'c':[0,2],'d':[0,3],'e':[0,4],'1':[0,5],'2':[0,6],'3':[0,7],
             'f':[1,0],'g':[1,1],'h':[1,2],'i':[1,3],'j':[1,4],'4':[1,5],'5':[1,6],'6':[1,7],
             'k':[2,0],'l':[2,1],'m':[2,2],'n':[2,3],'o':[2,4],'7':[2,5],'8':[2,6],'9':[2,7],
             'p':[3,0],'q':[3,1],'r':[3,2],'s':[3,3],'t':[3,4],'.':[3,5],'@':[3,6],'0':[3,7],
             'u':[4,0],'v':[4,1],'w':[4,2],'x':[4,3],'y':[4,4],'z':[4,5],'_':[4,6],'/':[4,7],
             'aA#':[5,0],' ':[5,1]}
    page2 = {'A':[0,0],'B':[0,1],'C':[0,2],'D':[0,3],'E':[0,4],'1':[0,5],'2':[0,6],'3':[0,7],
             'F':[1,0],'G':[1,1],'H':[1,2],'I':[1,3],'J':[1,4],'4':[1,5],'5':[1,6],'6':[1,7],
             'K':[2,0],'L':[2,1],'M':[2,2],'N':[2,3],'O':[2,4],'7':[2,5],'8':[2,6],'9':[2,7],
             'P':[3,0],'Q':[3,1],'R':[3,2],'S':[3,3],'T':[3,4],'.':[3,5],'@':[3,6],'0':[3,7],
             'U':[4,0],'V':[4,1],'W':[4,2],'X':[4,3],'Y':[4,4],'Z':[4,5],'_':[4,6],'/':[4,7],
             'aA#':[5,0],' ':[5,1]}
    page3 = {'^':[0,0],'~':[0,1],'?':[0,2],'!':[0,3],'\'':[0,4],'"':[0,5],'(':[0,6],')':[0,7],
             '-':[1,0],':':[1,1],';':[1,2],'+':[1,3],'&':[1,4],'%':[1,5],'*':[1,6],'=':[1,7],
             '<':[2,0],'>':[2,1],'€':[2,2],'£':[2,3],'$':[2,4],'¥':[2,5],'¤':[2,6],'\\':[2,7],
             '[':[3,0],']':[3,1],'{':[3,2],'}':[3,3],',':[3,4],'.':[3,5],'@':[3,6],'§':[3,7],
             '#':[4,0],'¿':[4,1],'¡':[4,2],'_':[4,6],'/':[4,7],
             'aA#':[5,0],' ':[5,1]}

    pages = {1:page1,2:page2,3:page3}

    def move_to_page(position,current_page,target_page):
        moves = shortest_distance(position,[5,0])
        if current_page == 3:
            if target_page == 1:
                moves += 1
            elif target_page == 2:
                moves += 2
        elif current_page == 2:
            if target_page == 1:
                moves += 2
            elif target_page == 3:
                moves += 1
        else:
            moves += target_page - current_page
        return moves

    def find_page(character):
        for page in pages:
            if character in pages[page]:
                return int(page)

    def shortest_distance(pos1,pos2):
        moves = 0
        delta_y = abs(pos2[0]-pos1[0])
        delta_x = abs(pos2[1]-pos1[1])
        return min(delta_x,8-delta_x)+min(delta_y,6-delta_y)

    debug = False
    sequence = list(words)
    moves = 0
    position = [0,0]
    current_page = 1

    for element in sequence:
        if element not in pages[current_page]:
            target_page = find_page(element)
            moves += move_to_page(position,current_page,target_page)
            position = [5,0]
            current_page = target_page
        target_position = pages[current_page][element]
        moves += shortest_distance(position,target_position)
        position = target_position
        moves += 1
    return moves

def tv_remote(words: str):

    class Mode:
        LOWERCASE, UPPERCASE, SYMBOLIC = range(3)
        mode = LOWERCASE

        def islowercase(self):
            return self.mode == self.LOWERCASE

        def isuppercase(self):
            return self.mode == self.UPPERCASE

        def isalphanumeric(self):
            return self.mode in [self.LOWERCASE, self.UPPERCASE]

        def issymbolic(self):
            return self.mode == self.SYMBOLIC

        def switch_to_new_mode(self, other):
            if isinstance(other, int):
                button_presses = (other - self.mode) % 3
                self.mode = other
                return button_presses

    def dist(pos1, pos2):
        return sum(min(abs(p1-p2), dim - abs(p1-p2)) for p1, p2, dim in zip(pos1, pos2, kb_dims))

    screen_keyboard_alphanumeric = "abcde123fghij456klmno789pqrst.@0uvwxyz_/⇑ "
    symbols_map_alphanumeric = {c: (i // 8, i % 8) for i, c in enumerate(screen_keyboard_alphanumeric)}
    kb_dims = (len(screen_keyboard_alphanumeric) // 8 + 1, 8)
    screen_keyboard_symbolic = "^~?!\'\"()-:;+&%*=<>€£$¥¤\[]{},.@§#¿¡ΦΨΩ_/⇑ "
    symbols_map_symbolic = {c: (i // 8, i % 8) for i, c in enumerate(screen_keyboard_symbolic)}
    common_symbols = "".join(set(screen_keyboard_alphanumeric) & set(screen_keyboard_symbolic))

    button_presses = 0
    cur_position, next_position = (0, 0), (0, 0)
    mode = Mode()

    words_to_type = []
    for ch in words:
        if ch in common_symbols:
            words_to_type.append(ch.lower())

        elif ch in screen_keyboard_symbolic:
            words_to_type.append("⇑"*mode.switch_to_new_mode(Mode.SYMBOLIC) + ch)

        elif ch.lower() in screen_keyboard_alphanumeric:
            if ch.isalpha():
                if ch.islower():
                    words_to_type.append("⇑"*mode.switch_to_new_mode(mode.LOWERCASE) + ch.lower())
                elif ch.isupper():
                    words_to_type.append("⇑"*mode.switch_to_new_mode(mode.UPPERCASE) + ch.lower())
            elif not mode.isalphanumeric():
                words_to_type.append("⇑"*mode.switch_to_new_mode(mode.LOWERCASE) + ch)
            else:
                words_to_type.append(ch)        # mode.isalphanumeric() and not ch.isalpha()

    words_to_type = "".join(words_to_type)

    for ch in words_to_type:
        if ch in screen_keyboard_alphanumeric:
            next_position = symbols_map_alphanumeric[ch]
            button_presses += 1 + dist(cur_position, next_position)
            cur_position = next_position

        elif ch in screen_keyboard_symbolic:
            next_position = symbols_map_symbolic[ch]
            button_presses += 1 + dist(cur_position, next_position)
            cur_position = next_position

        else:
            return -1

    return button_presses

def dist(a, b, w=8, h=6):
    ay, ax = divmod(a, w)
    by, bx = divmod(b, w)
    dx = min((ax - bx) % w, (bx - ax) % w)
    dy = min((ay - by) % h, (by - ay) % h)
    return dx + dy

def tv_remote(words):
    num_buttons = 0

    presses = []
    mode = 0
    keypads = [
        'abcde123fghij456klmno789pqrst.@0uvwxyz_/⇧ ––––––',
        'abcde123fghij456klmno789pqrst.@0uvwxyz_/⇧ ––––––'.upper(),
        '^~?!\'"()-:;+&%*=<>€£$¥¤\\[]{},.@§#¿¡–––_/⇧ ––––––'
    ]
    for c in words:
        while True:
            if c in keypads[mode]:
                presses.append(keypads[mode].index(c))
                num_buttons += 1  # OK
                break
            mode = (mode + 1) % len(keypads)
            presses.append(keypads[mode].index('⇧'))
            num_buttons += 1  # OK shift

    cursor = 0
    for press in presses:
        print(f'{num_buttons} button moves from {cursor} to {press}')
        num_buttons += dist(cursor, press)
        cursor = press
    
    return num_buttons

from typing import Tuple

KEYBOARDS = [
    [
        'abcde123',
        'fghij456',
        'klmno789',
        'pqrst.@0',
        'uvwxyz_/',
    ],
    [
        'ABCDE123',
        'FGHIJ456',
        'KLMNO789',
        'PQRST.@0',
        'UVWXYZ_/',
    ],
    [
        '^~?!\'\"()',
        '-:;+&%*=',
        '<>€£$¥¤\\',
        '[]{},.@§',
        '#¿¡   _/',
    ],
]

Position = Tuple[int, int]

SPACE = 5, 1
SWITCH_MODE = 5, 0


def get_pos(mode: int, c: str) -> Position:
    if c == ' ':
        return SPACE

    for i, chars in enumerate(KEYBOARDS[mode]):
        if c in chars:
            return i, chars.index(c)


def get_diff(a: Position, b: Position) -> int:
    (x1, y1), (x2, y2) = a, b
    x, y = abs(x1 - x2), abs(y1 - y2)
    return min(x, 6 - x) + min(y, 8 - y)


def mode_switch(mode: int, c: str, pos: Position) -> Tuple[int, int, Position]:
    if c == ' ' or c in ''.join(KEYBOARDS[mode]):
        return mode, 0, pos

    switches = get_diff(pos, SWITCH_MODE)
    while c not in ''.join(KEYBOARDS[mode]):
        switches += 1
        mode = (mode + 1) % len(KEYBOARDS)

    return mode, switches, SWITCH_MODE


def tv_remote(words: str):
    total = mode = 0
    pos = 0, 0
    for w in words:
        mode, switches, pos = mode_switch(mode, w, pos)
        new_pos = get_pos(mode, w)
        total += switches + get_diff(pos, new_pos) + 1
        pos = new_pos

    return total