---

# 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: EASY
Starter code: def tv_remote(words):
	

Solutions:
(H, W) = (6, 8)
KEYBOARD = ['abcde123fghij456klmno789pqrst.@0uvwxyz_/\x0e ', 'ABCDE123FGHIJ456KLMNO789PQRST.@0UVWXYZ_/\x0e ', '^~?!\'"()-:;+&%*=<>€£$¥¤\\[]{},.@§#¿¡\x0e\x0e\x0e_/\x0e ']
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]['\x0e'])
			was = '\x0e'
			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 = ('^~?!\'"()', '-:;+&%*=', '<>€£$¥¤\\', '[]{},.@§', '#¿¡\xa0\xa0\xa0_/')
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):
		temp_pos = (index // 8, index % 8)
		result = []
		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)
		self.current_pos = temp_pos
		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)
	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
				break
			mode = (mode + 1) % len(keypads)
			presses.append(keypads[mode].index('⇧'))
			num_buttons += 1
	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