class Solution1:
    def isPalindrome(self, s):
        s = s.lower().strip()
        sLen = len(s)
        if sLen == 0:
            return True
        left = 0
        right = sLen - 1
        while left < right:
            while left < right and not self.isalphanumeric(s[left]):
                left = left + 1
            while left  < right and not self.isalphanumeric(s[right]):
                right = right - 1
            if s[left] != s[right]:
                return False
            left = left + 1
            right = right - 1
        return True

    # 是否字符或数字
    def isalphanumeric(self,str):
        return str.isalpha() or str.isdigit()

s = Solution1()
arr = ["A man, a plan, a canal: Panama","race a car","   aba","9P"]
for str in arr:
    print(s.isPalindrome(str))


"""The Logitech Harmony Hub integration."""
import asyncio
import logging

from homeassistant.components.remote import (
    ATTR_ACTIVITY,
    ATTR_DELAY_SECS,
    DEFAULT_DELAY_SECS,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send

from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS
from .remote import HarmonyRemote

_LOGGER = logging.getLogger(__name__)

async def async_setup(hass: HomeAssistant, config: dict):
    """Set up the Logitech Harmony Hub component."""
    hass.data.setdefault(DOMAIN, {})

    return True

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
    """Set up Logitech Harmony Hub from a config entry."""
    # As there currently is no way to import options from yaml
    # when setting up a config entry, we fallback to adding
    # the options to the config entry and pull them out here if
    # they are missing from the options
    _async_import_options_from_data_if_missing(hass, entry)

    address = entry.data[CONF_HOST]
    name = entry.data[CONF_NAME]
    activity = entry.options.get(ATTR_ACTIVITY)
    delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)

    harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf")
    try:
        device = HarmonyRemote(
            name, entry.unique_id, address, activity, harmony_conf_file, delay_secs
        )
        connected_ok = await device.connect()
    except (asyncio.TimeoutError, ValueError, AttributeError) as err:
        raise ConfigEntryNotReady from err

    if not connected_ok:
        raise ConfigEntryNotReady

    hass.data[DOMAIN][entry.entry_id] = device

    entry.add_update_listener(_update_listener)

    for component in PLATFORMS:
        hass.async_create_task(
            hass.config_entries.async_forward_entry_setup(entry, component)
        )

    return True

@callback
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
    options = dict(entry.options)
    modified = 0
    for importable_option in [ATTR_ACTIVITY, ATTR_DELAY_SECS]:
        if importable_option not in entry.options and importable_option in entry.data:
            options[importable_option] = entry.data[importable_option]
            modified = 1

    if modified:
        hass.config_entries.async_update_entry(entry, options=options)

async def _update_listener(hass: HomeAssistant, entry: ConfigEntry):
    """Handle options update."""
    async_dispatcher_send(
        hass, f"{HARMONY_OPTIONS_UPDATE}-{entry.unique_id}", entry.options
    )

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
    """Unload a config entry."""
    unload_ok = all(
        await asyncio.gather(
            *[
                hass.config_entries.async_forward_entry_unload(entry, component)
                for component in PLATFORMS
            ]
        )
    )

    # Shutdown a harmony remote for removal
    device = hass.data[DOMAIN][entry.entry_id]
    await device.shutdown()

    if unload_ok:
        hass.data[DOMAIN].pop(entry.entry_id)

    return unload_ok


#  Created byMartin.cz

import numpy as np
from . enums import *
from . activations import Activation
from . layer import Layer
from . utils import *

class Conv2D(Layer):
    """Represents a 2D convolution layer of neural network."""

    def __init__(self, depth, ksize, stride=1, pad=VALID, activation=RELU, init_method=PLAIN):
        """
        Initializes a new instance of Conv2D.

        Args:
            depth: int
                Number of filters.

            ksize: int or (int, int)
                Size of the kernel as (n_h, n_w) or single integer if squared.

            stride: int or (int, int)
                Single step kernel shift as single value or (s_h, s_w).

            pad: str, int, (int, int) or (int, int, int, int)
                Initial data padding as 'valid' or 'same' mode or direct values
                as (p_h, p_w) or (p_t, p_b, p_l, p_r).

            activation: str or None
                Activation function name such as 'sigmoid', 'relu' or 'tanh'.
                If set to None, activation is not applied.

            init_method: str
                W initialization method name such as 'plain', 'xavier' or 'he'.
        """

        self._depth = int(depth)
        self._ksize = (ksize, ksize) if isinstance(ksize, int) else ksize
        self._stride = (stride, stride) if isinstance(stride, int) else stride
        self._pad = self._init_padding(pad, *self._ksize)
        self._activation = Activation.create(activation)
        self._init_method = init_method

        self._X = None
        self._W = None
        self._b = None
        self._dW = None
        self._db = None
        self._cols = None

    def __str__(self):
        """Gets string representation."""

        activation = ""
        if self._activation is not None:
            activation = "|%s" % self._activation

        return "Conv2D(%dx%dx%d%s)" % (self._ksize[0], self._ksize[0], self._depth, activation)

    def initialize(self, shape):
        """
        Clears caches and re-initializes params.

        Args:
            shape: (int,)
                Expected input shape. The shape must be provided without first
                dimension for number of samples (m).

        Returns:
            (int,)
                Output shape. The shape is provided without first dimension for
                number of samples (m).
        """

        # clear params and caches
        self._X = None
        self._W = None
        self._b = None
        self._dW = None
        self._db = None
        self._cols = None

        # get dimensions
        h_in, w_in, c_in = shape
        f_h, f_w = self._ksize
        h_out, w_out, c_out = self.outshape(shape)

        # init params
        self._init_params(f_h, f_w, c_in, c_out)

        # return output shape
        return h_out, w_out, c_out

    def forward(self, X, training=None, **kwargs):
        """
        Performs forward propagation using activations from previous layer.

        Args:
            X: np.ndarray
                Input data/activations from previous (left) layer.
                The expected shape is (m, n_h, n_w, n_c).

            training: bool
                If set to True, the input data/activations are considered as
                training set.

        Returns:
            Calculated activations from this layer.
        """

        self._X = X

        # get dimensions
        m, h_in, w_in, c_in = X.shape
        h_out, w_out, c_out = self.outshape(X.shape[1:])
        f_h, f_w = self._ksize

        # init params
        if self._W is None:
            self._init_params(f_h, f_w, c_in, c_out)

        # apply convolution
        X = X.transpose(0, 3, 1, 2)
        self._cols = im2col(X, self._ksize, self._pad, self._stride)
        W = self._W.transpose(3, 2, 0, 1)
        Z = W.reshape((c_out, -1)).dot(self._cols)
        Z = Z.reshape(c_out, h_out, w_out, m)
        Z = Z.transpose(3, 1, 2, 0) + self._b

        # apply activation
        if self._activation is not None:
            return self._activation.forward(Z)

        return Z

    def backward(self, dA, lamb=0, **kwargs):
        """
        Performs backward propagation using upstream gradients.

        Args:
            dA:
                Gradients from previous (right) layer.
                The expected shape is (m, n_h, n_w, n_c).

            lamb: float
                Lambda parameter for L2 regularization.

        Returns:
            Gradients from this layer.
        """

        # get dimensions
        m, h_out, w_out, c_out = dA.shape

        # apply activation
        dZ = dA
        if self._activation is not None:
            dZ = self._activation.backward(dA)

        # calc gradients
        dZ = dZ.transpose(3, 1, 2, 0).reshape(c_out, -1)
        W = np.transpose(self._W, (3, 2, 0, 1))
        dW = dZ.dot(self._cols.T).reshape(W.shape)

        self._dW = (1 / m) * np.transpose(dW, (2, 3, 1, 0)) + (lamb / m) * self._W
        self._db = (1 / m) * dZ.sum(axis=(0, 1, 2))

        # apply reversed convolution
        cols = W.reshape(c_out, -1).T.dot(dZ)
        dX = col2im(cols, np.moveaxis(self._X, -1, 1).shape, self._ksize, self._pad, self._stride)
        dX = np.transpose(dX, (0, 2, 3, 1))

        return dX

    def update(self, *params):
        """
        Updates layer params.

        Args:
            *params: (np.ndarray,)
                Params to update as W and b.
        """

        self._W = params[0]
        self._b = params[1]

    def outshape(self, shape):
        """
        Calculates output shape.

        Args:
            shape: (int,)
                Expected input shape. The shape must be provided without first
                dimension for number of samples (m).

        Returns:
            (int,)
                Output shape. The shape is provided without first dimension for
                number of samples (m).
        """

        h_in, w_in, c_in = shape
        f_h, f_w = self._ksize
        p_t, p_b, p_l, p_r = self._pad
        s_h, s_w = self._stride
        h_out = int(1 + (h_in - f_h + p_t + p_b) / s_h)
        w_out = int(1 + (w_in - f_w + p_l + p_r) / s_w)
        c_out = self._depth

        return h_out, w_out, c_out

    def paramcount(self, shape):
        """
        Calculates number of trainable params.

        Args:
            shape: (int,)
                Expected input shape. The shape must be provided without first
                dimension for number of samples (m).

        Returns:
            int
                Number of trainable params.
        """

        # get dimensions
        h_in, w_in, c_in = shape
        f_h, f_w = self._ksize
        h_out, w_out, c_out = self.outshape(shape)

        # count params
        w = f_h * f_w * c_in * c_out
        b = c_out

        return w + b

    def parameters(self):
        """Gets all layer parameters."""

        return self._W, self._b

    def gradients(self):
        """Gets all layer gradients."""

        return self._dW, self._db

    def loss(self):
        """
        Calculates regularization loss.

        Returns:
            float
                Regularization loss.
        """

        return np.sum(np.square(self._W))

    def _zero_pad(self, x):
        """Apply zero-padding."""

        p_t, p_b, p_l, p_r = self._pad

        return np.pad(x, ((0, 0), (p_t, p_b), (p_l, p_r), (0, 0)),
            mode = 'constant',
            constant_values = (0, 0))

    def _init_padding(self, pad, f_h, f_w):
        """Initialize padding."""

        if isinstance(pad, int):
            return pad, pad, pad, pad

        if pad == VALID:
            return 0, 0, 0, 0

        if pad == SAME:
            p_t = (f_h - 1) // 2
            p_b = p_t
            p_l = (f_w - 1) // 2
            p_r = p_l

            if not self._ksize[0] % 2:
                p_b += 1

            if not self._ksize[1] % 2:
                p_r += 1

            return p_t, p_b, p_l, p_r

        if len(pad) == 2:
            return pad[0], pad[0], pad[1], pad[1]

        if len(pad) == 4:
            return pad

        raise ValueError("Incorrect padding! -> %s" % str(pad))

    def _init_params(self, f_h, f_w, c_in, c_out):
        """Initializes params."""

        # init weights
        if self._init_method == PLAIN:
            self._W = np.random.randn(f_h, f_w, c_in, c_out) * 0.01

        elif self._init_method == XAVIER:
            self._W = np.random.randn(f_h, f_w, c_in, c_out) * np.sqrt(1 / c_in)

        elif self._init_method == HE:
            self._W = np.random.randn(f_h, f_w, c_in, c_out) * np.sqrt(2 / c_in)

        else:
            raise ValueError("Unknown initialization method specified! -> '%s" % self._init_method)

        # init bias
        # self._b = np.zeros(c_out)
        self._b = np.random.randn(c_out)


#Uses python3

import sys
import numpy as np

def lcs2(a, b):
    #write your code here
    lcs = np.zeros([len(a)+1, len(b)+1])
    for i in range(1, len(a)+1):
        for j in range(1, len(b)+1):
            if a[i-1] == b[j-1]:
                lcs[i, j] = lcs[i-1, j-1] + 1
            else:
                lcs[i, j] = max(lcs[i, j-1], lcs[i-1, j])

    return lcs[-1,-1]

if __name__ == '__main__':
    input = sys.stdin.read()
    data = list(map(int, input.split()))

    n = data[0]
    data = data[1:]
    a = data[:n]

    data = data[n:]
    m = data[0]
    data = data[1:]
    b = data[:m]

    print(lcs2(a, b))


h, w, x, y = [int(x) for x in input().split()]
area1 = 0
area2 = 0
if h%x == 0:
    w1 = (h // x) * y
    h1 = h
    if w1 <= w:
        area1 = h1 * w1
if w%y == 0:
    h2 = (w // y) * x
    w2 = w
    if h2 <= h:
        area2 = h2 * w2
#print(area1, area2)
if area1 == 0 and area2 == 0:
    print(0, 0)
elif area1 >= area2 :
    print(h1, w1)
elif area2 <= area1 :
    print(h2, w2)
else:
    print(0, 0)


import sys
import os
import glob
import numpy as np
from pyMCDS import pyMCDS
import matplotlib.pyplot as plt

argc = len(sys.argv)-1
print("# args=",argc)

if (argc < 1):
#  data_dir = int(sys.argv[kdx])
  print("Usage: provide output subdir")
  sys.exit(-1)

#data_dir = 'output'
kdx = 1
data_dir = sys.argv[kdx]
print('data_dir = ',data_dir)
os.chdir(data_dir)
#xml_files = glob.glob('output/output*.xml')
xml_files = glob.glob('output*.xml')
os.chdir('..')
xml_files.sort()
#print('xml_files = ',xml_files)

ds_count = len(xml_files)
print("----- ds_count = ",ds_count)
mcds = [pyMCDS(xml_files[i], data_dir) for i in range(ds_count)]  # spews lots of prints

#print(mcds[0].get_substrate_names())
# ['virion', 'assembled virion', 'interferon 1', 'pro-inflammatory cytokine', 'chemokine', 'debris', 'pro-pyroptosis cytokine', 'anti-inflammatory cytokine', 'collagen']

for idx in range(ds_count):
  # print(np.isnan([ (mcds[idx].get_concentrations('virion')).sum()  for idx in range(ds_count)] )
  if True in np.isnan([ (mcds[idx].get_concentrations('virion')) ]):
    print("------- found NaN in file # ",idx)


#!/usr/bin/env python
import argparse
import json
import os
import sys
import re
import requests
import shutil
sys.path.append(os.path.join(os.path.dirname(__file__), '../libs'))
from core.storage_client_factory import StorageClientFactory
from core.automan_client import AutomanClient

TEMP_DIR = '/temp'

def get_cv_color(colors, name):
    color = colors[name]
    cv_color = (int(color[5:7], 16), int(color[3:5], 16), int(color[1:3], 16))
    return cv_color

class AutomanArchiver(object):

    @classmethod
    def archive(cls, automan_info, archive_info):
        print(archive_info)

        annotations_dir = os.path.join(TEMP_DIR, 'Annotations')
        images_dir = os.path.join(TEMP_DIR, 'Images')
        image_annotations_dir = os.path.join(TEMP_DIR, 'Images_Annotations')

        # whether or not to write image in bag file to image files
        is_including_image = archive_info.get('include_image', False)

        max_frame = cls.__get_frame_range(
            automan_info, archive_info['project_id'], archive_info['annotation_id'])
        colors = cls.__get_annotation_color(automan_info, archive_info['project_id'])
        candidates = cls.__get_candidates(
            automan_info, archive_info['project_id'], archive_info['original_id'])
        for i in range(max_frame):
            annotation = cls.__get_annotation(
                automan_info, archive_info['project_id'], archive_info['annotation_id'], i + 1, annotations_dir)
            if is_including_image:
                for candidate in candidates:
                    file_name = cls.__get_annotation_image(
                        automan_info, archive_info['project_id'],
                        archive_info['dataset_id'], candidate['id'], i + 1, candidate['ext'], images_dir)
                    if file_name is not None:
                        cls.__draw_annotation(file_name, annotation, colors, images_dir, image_annotations_dir)

    @staticmethod
    def __get_frame_range(automan_info, project_id, annotation_id):
        path = '/projects/' + str(project_id) + '/annotations/' + str(annotation_id) + '/'
        res = AutomanClient.send_get(automan_info, path).json()
        dataset_path = '/projects/' + str(project_id) + '/datasets/' + str(res['dataset_id']) + '/'
        dataset = AutomanClient.send_get(automan_info, dataset_path).json()
        return dataset['frame_count']

    @staticmethod
    def __get_candidates(automan_info, project_id, original_id):
        path = '/projects/' + str(project_id) + '/originals/' + str(original_id) + '/candidates/'
        res = AutomanClient.send_get(automan_info, path).json()
        candidates = []
        for record in res['records']:
            ext = '.jpg' if record['data_type'] == 'IMAGE' else '.pcd'
            candidates.append({'id': record['candidate_id'], 'ext': ext})
        return candidates

    @staticmethod
    def __get_annotation(automan_info, project_id, annotation_id, frame, annotations_dir):
        path = '/projects/' + str(project_id) + '/annotations/' + str(annotation_id) \
            + '/frames/' + str(frame) + '/objects/'
        res = AutomanClient.send_get(automan_info, path).json()
        # TODO format to "kitti format"

        # ensure directory
        os.makedirs(annotations_dir, exist_ok=True)
        with open(os.path.join( annotations_dir, str(frame).zfill(6) + '.json'), mode='w') as frame:
            frame.write(json.dumps(res))
        return res

    @staticmethod
    def __get_annotation_image(automan_info, project_id, dataset_id, candidate_id, frame, ext, images_dir):
        path = '/projects/' + str(project_id) + '/datasets/' + str(dataset_id) \
            + '/candidates/' + str(candidate_id) + '/frames/' + str(frame) + '/'
        img_url = AutomanClient.send_get(automan_info, path).text
        if re.search(automan_info['host'], img_url):
            headers = {
                'Authorization': 'JWT ' + automan_info['jwt'],
            }
        else:
            headers = {}
        res = requests.get(img_url, headers=headers)
        if 200 > res.status_code >= 300:
            print(f'get annotation image status_code = {res.status_code}. body = {res.text}')
            return None

        # write images
        os.makedirs(images_dir, exist_ok=True)
        file_name = str(candidate_id) + '_' + str(frame).zfill(6) + ext
        img_path = os.path.join(images_dir, file_name)
        with open(img_path, mode='wb') as frame:
            frame.write(res.content)
        if ext == '.jpg':
            return file_name
        return None

    @staticmethod
    def __draw_annotation(file_name, annotation, colors, images_dir, image_annotations_dir):
        import cv2

        if annotation['count'] == 0:
            return 0

        os.makedirs(image_annotations_dir, exist_ok=True)
        img = cv2.imread(os.path.join(images_dir, file_name))
        for a in annotation['records']:
            for c in a['content']:
                if 'min_x_2d' not in a['content'][c]:
                    continue
                bbox = ((a['content'][c]['min_x_2d'], a['content'][c]['min_y_2d']),
                        (a['content'][c]['max_x_2d'], a['content'][c]['max_y_2d']))
                cv_color = get_cv_color(colors, a['name'])
                cv2.rectangle(img, bbox[0], bbox[1], cv_color, 2)
        cv2.imwrite(os.path.join(image_annotations_dir, file_name), img)

    @staticmethod
    def __get_annotation_color(automan_info, project_id):
        path = '/projects/' + str(project_id) + '/'
        res = AutomanClient.send_get(automan_info, path).json()
        colors = {}
        for record in res['klassset']['records']:
            config = json.loads(record['config'])
            colors[record['name']] = config['color']
        return colors

def main(automan_info, archive_info, storage_type, storage_info):
    automan_info = json.loads(automan_info)
    archive_info = json.loads(archive_info)

    archive_dir = archive_info['archive_dir'].rstrip('/') + '/'

    storage_client = StorageClientFactory.create(
        storage_type,
        json.loads(storage_info),
        archive_info
    )

    AutomanArchiver.archive(automan_info, archive_info)
    shutil.make_archive(
        archive_dir + archive_info['archive_name'],
        'gztar',
        root_dir=TEMP_DIR)
    if storage_type == 'AWS_S3':
        storage_client.upload(automan_info, archive_dir)

    # TODO post : ArchiviedLabelDataset
    data = {
        'file_path': archive_info['archive_dir'],
        'file_name': archive_info['archive_name'] + '.tar.gz',
        'annotation_id': archive_info['annotation_id'],
    }
    AutomanClient.send_result(automan_info, data)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--storage_type', required=False)
    parser.add_argument('--storage_info', required=False)
    parser.add_argument('--automan_info', required=True)
    parser.add_argument('--archive_info', required=True)
    args = parser.parse_args()
    main(args.automan_info, args.archive_info, args.storage_type, args.storage_info)


#TAG Chess AI
init='''
♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
· · · · · · · ·
· · · · · · · ·
· · · · · · · ·
· · · · · · · ·
♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜'''
#
vacant ='·'
black='♙♖♘♗♕♔'
white='♟♜♞♝♛♚'
pawn  ='♟♙'
rook  ='♜♖'
knight='♞♘'
bishop='♝♗'
king  ='♚♔'
queen ='♛♕'
#
def group(*g):#g is vararg of strings
    from itertools import product#cartesian product
    return {x:y for x,y in product(''.join(g),g)if x in y}
piece   =group(pawn,rook,knight,bishop,king,queen)#{'♔':king,'♟':pawn,'♚':king, ... }
color   =group(black,white)#{'♔':black,'♘':black,'♚':white, ... }
value   ={pawn:1,rook:5,knight:3,bishop:3,king:100,queen:9}
opponent={white:black,black:white}
#
def b2m(b):#board to matrix (make it mutable)
    b=b[1:]#get rid of the newline prefix
    return [x.split(' ')for x in b.split('\n')]
def m2b(m):#matrix to board (make it immutable)
    return '\n'+'\n'.join(' '.join(x)for x in m)
def move(b,x0,y0,x1,y1):#one-based from the bottom left
    assert legal(b, x0, y0, x1, y1),'illegal move'
    m=b2m(b)
    m[-y1][x1-1]=p=m[-y0][x0-1]
    assert p in piece,repr(p)+' is not a piece'
    m[-y0][x0-1]=vacant
    if p in pawn and y1 in{1,8}:#Not supported: promoting to non-queen
        m[-y1][x1-1],=set(queen)&set(color[p])#promote pawn to queen if pawn in a back row
    return m2b(m)
def flipb(b):#flip board:black/white's board positions
    return m2b(reversed([reversed(x)for x in b2m(b)]))
def flipm(*m):#flip move, *m in form x0,y0,x1,y1
    return (9-x for x in m)
def score(b,c):#the total value of all color c's pieces on board b
    return sum(value[piece[x]]for x in b if x in c)
def legal(b,x0,y0,x1,y1,print=identity):
    #Not supported: castling, en-passant
    m=b2m(b)
    if not 1<=x0<=8 or not 1<=y0<=8 or not 1<=x1<=8 or not 1<=y1<=8 or x0==x1 and y0==y1:#0 or 1 is off the board or piece didn't move
        print('Out of bounds or no movement')
        return False
    p0=m[-y0][x0-1]#piece before
    if p0 in black:
        m,x0,y0,x1,y1=b2m(flipb(b)),*flipm(x0,y0,x1,y1)#flip everything to make life easier
    try:
        p1=m[-y1][x1-1]#piece after
    except:
        print(-y1,x1-1)
        print(m)
        assert False,'Error?'
    Δ={abs(x1-x0),abs(y1-y0)}
    if p0 in queen:#Queen can act as either bishop or rook
        print('Queen: Δ='+str(Δ))
        if 0 in Δ   :p0,=set(color[p0])&set(rook)
        elif len(Δ)==1:p0,=set(color[p0])&set(bishop)
        else:print('Queen neither acting as rook nor bishop, Δ='+str(Δ));return False
    if p0 in pawn:
        print('Pawn')
        if abs(x0-x1)>1:#Invalid horizontal movement
            print('Pawn cannot move horizontal')
            return False
        if x0==x1:#Not capturing a piece
            if y0==2 and y1==4:return p1 is vacant and m[-3][x0-1] is vacant#Move forward twice
            if y1==y0+1:       return p1 is vacant                          #Move forward once
            return False                                                    #Invalid move
        return y1==y0+1 and p1 in opponent[color[p0]]#Capturing a piece
    if p0 in rook:
        print('Rook')
        if x0==x1:#We moved on y axis
            x,={x0,x1}
            for y in range(min(y0,y1),max(y0,y1))[1:]:#make sure path BETWEEN 0 and 1 is vacant
                if m[-y][x-1] is not vacant:return False
            return p1 in vacant+opponent[color[p0]]#end must be opponent piece or vacant
        if y0==y1:#We moved on x axis
            y,={y0,y1}
            for x in range(min(x0,x1),max(x0,x1))[1:]:#make sure path BETWEEN 0 and 1 is vacant
                if m[-y][x-1] is not vacant:return False
            return p1 in vacant+opponent[color[p0]]#end must be opponent piece or vacant
        return False
    if p0 in knight:
        print('Knight')
        if not Δ=={1,2}:return False
        return p1 in vacant+opponent[color[p0]]
    if p0 in bishop:
        print('Bishop')
        if len(Δ)>1:return False#Not an equal difference
        Δ=Δ.pop()
        assert Δ,'Internal logic error: Piece should be guarenteed to move'
        for _ in range(Δ-1):
            x0+=1 if x0<x1 else -1
            y0+=1 if y0<y1 else -1
            if m[-y0][x0-1] is not vacant:return False#Must be clear path to target
        return p1 in vacant+opponent[color[p0]]
    if p0 in king:
        print('King')
        return Δ<={0,1} and p1 in vacant+opponent[color[p0]]
    print('Piece not recognized: '+repr(p0))
    return False
def moves(b,x,y):
    #Returns the set of all possible resulting legal boards from moving that piece
    p=b2m(b)[-y][x-1]
    Δ=set()#set of different movements to try
    r=set(range(-7,8))#max possible movement range on the board
    if p in pawn        :Δ|={(0,1),(0,2),(1,1),(-1,1),(0,-1),(0,-2),(1,-1),(-1,-1)}
    if p in rook  +queen:Δ|={(0,n)for n in r}|{( n,0)for n in r}
    if p in bishop+queen:Δ|={(n,n)for n in r}|{(-n,n)for n in r}
    if p in knight      :Δ|={(1,2),(1,-2),(-1,2),(-1,-2),( 2,1),( 2,-1),(-2, 1),(-2,-1)}
    if p in king        :Δ|={(1,0),(1, 1),( 0,1),(-1, 1),(-1,0),(-1,-1),( 0,-1),( 1,-1)}
    out=set()
    for Δx,Δy in Δ:
        args=b,x,y,x+Δx,y+Δy
        if legal(*args):
            yield move(*args)#it's safe to assume every element is unique
def shuffled(l):
    from random import shuffle
    l=list(l)+[]
    shuffle(l)
    return l
def all_moves(b,c):#board,color
    out=set()
    m=b2m(b)
    for x in shuffled(range(1,9)):
        for y in shuffled(range(1,9)):
            if m[-y][x-1] in c:
                for move in moves(b,x,y):
                    if move not in out:
                        yield move
                        out|={move}
def all_n_moves(b,c,n):
    #Up to n moves
    out={b}
    for _ in range(n):
        new_out=out|set()
        for x in out:
            for move in all_moves(x,c):
                if move not in new_out:
                    yield move#using yield is ABSOLUTELY nessecary for performance! Without it, evaluating can take foorrreeevveerrr...like feeling the FULL blunt hit of the combinatorial explostion blah blah u get it right?  yield actually pauses the code here until it's used later'
                    new_out|={move}
        out=new_out
def advantage(b,c):
    return score(b,c)-score(b,opponent[c])
#
def think_1(b,c,bad=9999):
    ba=-9999#*1+advantage(b,c)#biggest advantage seen
    for s in search(b,c):#for search-board in search
        a=advantage(s,c)
        if a>ba:
            out=s
            ba=a
        if a>=bad:#The score is too high! Ignore it
            return None,bad
    return out,ba#move where c has most advantage; c's advantage
def think_2(b,c,bad=-9999):
    sa=9999#smallest advantage seen
    for s in search(b,c):#for search-board in search
        _,a=think_1(s,opponent[c],bad=sa)
        if a<sa:
            out=s
            sa=a
        if a<=bad:
            return None,bad
    return out,sa#move where c's opponent has least advantage; c's largest advantage = -(c's opponent's smallest advantage)
def think_3(b,c,bad=9999):
    ba=-9999#biggest advantage seen
    for s in search(b,c):#for search-board in search
        _,a=think_2(s,opponent[c],bad=ba)
        if a>ba:
            out=s
            ba=a
        if a>=bad:
            return None,bad
    return out,ba#move where c's opponent has least advantage; c's largest advantage = -(c's opponent's smallest advantage)
def think_4(b,c,bad=-9999):
    sa=9999#smallest advantage seen
    for s in search(b,c):#for search-board in search
        _,a=think_3(s,opponent[c],bad=sa)
        if a<sa:
            out=s
            sa=a
        if a<=bad:
            return None,bad
    return out,sa#move where c's opponent has least advantage; c's largest advantage = -(c's opponent's smallest advantage)

play=lambda *m:printed(think_4(move(ans,*m),black))[0]
search=lambda b,c:all_n_moves(b,c,1)
#DEMO:
#    ⮤ init
#    ⮤ play(5,2,5,4)
#    ⮤ play(4,2,4,4)
#    ⮤ play(4,4,4,5)
#    ⮤ play(2,1,3,3)
#    ⮤ ...(etc)...


import json
from collections import namedtuple

# csv.field_size_limit(sys.maxsize)

# direct means you have a representation that can appear directly in code
SerializeResult = namedtuple('SerializeResult', ['type_name', 'value', 'direct'])

class Converter:
    def __init__(self):
        self.supported_types = {}

    def register_type(self, type_name, type_type: type, serializer, deserializer):
        """
        Add extra type support, or overwrite the default serializer/deserializer
        We register a certain type name to avoid possible trouble from serializing & deserializing a type object
        Args:
            type_name:
            type_type:
            serializer: a serializer that takes an object of certain type, and returns the type name and string representation of the object
            deserializer: deserializer takes a string and returns the object of specified type
        """
        self.supported_types[type_name] = (type_type, serializer, deserializer)

    def serialize(self, object_data) -> SerializeResult:
        for type_name, funcs in self.supported_types.items():
            if isinstance(object_data, funcs[0]):
                return funcs[1](object_data)

        raise Exception(f'Unsupported data type {type(object_data)}, {object_data}')

    def deserialize(self, type_name, value):
        funcs = self.supported_types[type_name]
        return funcs[2](value)

converter = Converter()
converter.register_type('str', str, lambda a: SerializeResult('str', a, True), lambda a: a)
converter.register_type('None', type(None), lambda a: SerializeResult('None', '', True), lambda s: None)
converter.register_type('dict', dict, lambda a: SerializeResult('dict', json.dumps(a), True), lambda a: json.loads(a))