Разработка на коленке

"тут должна быть красивая цитата о программировании"

Рефакторинг битвы против Skynet

2016-08-31 19:47
skynet

На codingame решил все задачи из блока easy и перешёл к medium. Первой задачей "средней сложности", стала разработка вируса для битвы со Skynet. Ну а по сути, нужно было не дать боту достигнуть одной из целевых вершин (шлюзы) в ненаправленном графе.

Моя стратегия обороны была основана на том, что одна вершина не может быть напрямую связана более чем с одним шлюзом. Значит можно просто обрубать связи бот-шлюз, если такие есть, в противном случае выключать любую связь бот-вершина.

Как и раньше, при разработке решения основная цель - освоение OCaml. Исходя из этого, хотел сделать для графа свою структуру с вершинами и связями, чтобы поработать с определением новых типов, но потом отказался от этого в пользу матрицы смежности, реализованной на массивах целых чисел. Впрочем, похожего результата можно было добиться с помощью других структур данных, но я не стал долго возиться с выбором.

Первое решение

Вот так выглядел первый вариант решения, который смог победить бота. Несмотря на то, что с задачей этот код справляется, меня он не устраивал. Если бы писал бота для игры, то поддержка такого кода вызывала бы только нехорошие мысли об авторе. Поэтому я задумал рефакторинг.

let read_network_config () =
    let init_data = read_line () in
    Scanf.sscanf init_data "%d %d %d" (fun n l e -> (n, l, e))


let create_disconnected_network nodes_number =
    Array.make_matrix nodes_number nodes_number 0


let connect_nodes network edges_number =
    for i = 0 to edges_number - 1 do
        let edge = read_line () in
        let n1, n2 = Scanf.sscanf edge "%d %d" (fun n1 n2 -> (n1, n2)) in
        network.(n1).(n2) <- 1;
        network.(n2).(n1) <- 1;
    done


let read_gateways gateway_number =
     Array.init gateway_number (fun i -> read_int ())


let search_nodes_to_disconnect network gateways skynet_agent_position =
    let to_destroy = Array.map (fun i -> if network.(i).(skynet_agent_position) = 1 then (Some i, Some skynet_agent_position) else (None, None)) gateways in
    let e = Array.fold_left (fun a b ->
        match a, b with
        | (None, None), (None, None) -> (None, None)
        | (Some x, Some y), (None, None) | (None, None), (Some x, Some y) -> (Some x, Some y)
    ) (None, None) to_destroy in

    let rec find_edge e =
        if e < Array.length network then
            if network.(skynet_agent_position).(e) = 1 then
                (
                    network.(skynet_agent_position).(e) <- 0;
                    network.(e).(skynet_agent_position) <- 0;
                    e
            )
            else find_edge (e + 1)
        else
            0
    in

    match e with
    | (None, None) -> (skynet_agent_position, find_edge 0)
    | (Some x, Some y) -> (
        network.(x).(y) <- 0;
        network.(y).(x) <- 0;
        (x, y)
    )


let disconnect_nodes (a, b) =
    Printf.printf "%d %d\n" a b


let () =
    let nodes_number, edges_number, gateway_number = read_network_config () in

    let network = create_disconnected_network nodes_number in
    connect_nodes network edges_number;

    let gateways = read_gateways gateway_number in

    while true do
        read_int () |> search_nodes_to_disconnect network gateways |> disconnect_nodes
    done

Рефакторинг

При рефакторинге для меня важно было добиться логичной структуры программы, читабельности и спрятать реализацию графа либо в модуль, либо просто в список функций. Делать класс не видел необходимости, потому что у меня не было потребности в динамическом связывании.

После рефакторинга

module Net = struct
    let connect network a b =
        network.(a).(b) <- 1;
        network.(b).(a) <- 1;
        ()

    let disconnect network a b =
        network.(a).(b) <- 0;
        network.(b).(a) <- 0;
        ()

    let has_direct_connection network a b =
        network.(a).(b) = 1

    let find_any_connected_node network node_number =
        let rec find num =
            if num < Array.length network then
                if network.(node_number).(num) = 1 then
                    num
                else
                    find (num + 1)
            else
                node_number
        in

        find 0

    let build nodes_number edges =
        let network = Array.make_matrix nodes_number nodes_number 0 in
        List.iter (fun (a, b) -> connect network a b) edges;
        network
end


let read_network_config () =
    let open Scanf in

    let nodes_number, edges_number, gateway_number =
        let init_data = read_line () in
        sscanf init_data "%d %d %d" (fun n l e -> (n, l, e))
    in

    let rec read_edges = function
        | 0 -> []
        | num -> sscanf (read_line ()) "%d %d" (fun a b -> (a, b)) :: read_edges (num - 1)
    in

    let edges = read_edges edges_number in

    let gateways =
        Array.init gateway_number (fun i -> read_int ()) |> Array.to_list
    in

    nodes_number, edges, gateways


let fight_with_skynet network gateways =
    let destroy_edge a b =
        Net.disconnect network a b;
        Printf.printf "%d %d\n" a b
    in

    let rec stop_skynet_agent () =
        let skynet_agent_position = read_int () in
        let agent_to_gateway_edges =
            List.filter (fun gateway ->
                Net.has_direct_connection network skynet_agent_position gateway)
            gateways
        in
        let connected_node = match agent_to_gateway_edges with
        | [] -> Net.find_any_connected_node network skynet_agent_position
        | connected_gateway :: tl -> connected_gateway
        in
        destroy_edge skynet_agent_position connected_node;

        stop_skynet_agent ()
    in

    stop_skynet_agent ()


let () =
    let nodes_number, edges, gateways = read_network_config () in
    let network = Net.build nodes_number edges in
    fight_with_skynet network gateways

Заключение

В итоге у меня получилось решение, которое и задачу решало, и меня устраивало (во всяком случае, с текущем уровнем знаний в OCaml).

Недостаток решения задач на соревнованиях - нужен только результат, а не поддерживаемый код. Довольно сложно убедить себя в необходимости дорабатывать код, если все тесты пройдены, а возвращаться к коду вряд ли понадобится.