Eloquent JavaScript #07# Project: A Robot

索引

註釋即筆記:瀏覽器

const roads = [
    "Alice's House-Bob's House", "Alice's House-Cabin",
    "Alice's House-Post Office", "Bob's House-Town Hall",
    "Daria's House-Ernie's House", "Daria's House-Town Hall",
    "Ernie's House-Grete's House", "Grete's House-Farm",
    "Grete's House-Shop", "Marketplace-Farm",
    "Marketplace-Post Office", "Marketplace-Shop",
    "Marketplace-Town Hall", "Shop-Town Hall"
];

function buildGraph(edges) {
    // graph的存儲格式相似於
    // graph.place_a: [place_b, place_c]
    // 表示由a可以直接到達b或者c
    let graph = Object.create(null);

    function addEdge(from, to) {
        // 首先判斷該起點有沒有被添加
        if(graph[from] == null) {
            graph[from] = [to];
        } else {
            graph[from].push(to);
        }
    }

    // 'Alice's House-Bob's House'.split("-")
    // → ['Alice's House', 'Bob's House']
    for(let [from, to] of edges.map(r => r.split("-"))) {
        addEdge(from, to);
        addEdge(to, from);
    }

    return graph;
}

const roadGraph = buildGraph(roads);

/**
 * 不要條件反射地把每一個概念都搞成對象,
 * 這樣的程序一般是難以理解和維護的。
 * 相反,用最小的數值集合來描述村莊&機器人
 * 狀態就能夠了。
 */

class VillageState {
    constructor(place, parcels) {
        this.place = place;
        this.parcels = parcels;
    }

    move(destination) {
        // move表示機器人的一次移動。
        // 首先檢查是否存在一條從當前位置this.place到目的地destination的路
        // 若是不存在就說明不是合法的移動,返回舊的VillageState。
        // 若是存在,就更新機器人的當前位置,也就是更新VillageState.place
        // 爲destination,固然同時須要更新包裹的狀態:1-還沒被機器人拿到
        // 的包裹不去管它,2-拿到的包裹則更新當前位置place(目的地address不改變)
        // 3-最後過濾掉已經送到的包裹(目的地就在本地)
        // PS. 整個move方法其實是重造了一個VillageState,並沒改變舊的
        // VillageState對象
        if(!roadGraph[this.place].includes(destination)) {
            return this;
        } else {
            let parcels = this.parcels.map(p => {
                if(p.place != this.place) return p;
                return {
                    place: destination,
                    address: p.address
                };
            }).filter(p => p.place != p.address);
            return new VillageState(destination, parcels);
        }
    }
}

/**
 * 能夠用Object.freeze凍結對象
 * 全部對該對象屬性的寫入操做會被忽略
 * 這須要計算機作一些額外工做
 * let object = Object.freeze({value: 5});
    object.value = 10;
    console.log(object.value);
    // → 5
 */

/**
 * But the most important limit 
 * on what kind of systems we can build 
 * is how much we can understand. 
 * Anything that makes your code easier 
 * to understand makes it possible 
 * to build a more ambitious system.
 * 寫程序最重要的限制是咱們可以理解多少
 */

// robot其實是一個函數接口,
// 該函數輸入state和memory
// 輸出決策action用於實際移動
// 這樣寫就可以動態更改策略了。
function runRobot(state, robot, memory) {
    for(let turn = 0;; turn++) {
        if(state.parcels.length == 0) {
            console.log(`Done in ${turn} turns`);
            break;
        }
        let action = robot(state, memory);
        state = state.move(action.direction);
        memory = action.memory;
        console.log(`Moved to ${action.direction}`);
    }
}

function randomPick(array) {
    let choice = Math.floor(Math.random() * array.length);
    return array[choice];
}

// 最愚蠢但有效的策略--隨機決定下一個方向。
// 雖然這裏只有一個參數,可是js容許傳
// 入更多(或者更少)的參數。在這裏,傳入的
// memeory被忽略了
function randomRobot(state) {
    return {
        direction: randomPick(roadGraph[state.place])
    };
}

// 初始化
VillageState.random = function(parcelCount = 5) {
    let parcels = [];
    for(let i = 0; i < parcelCount; i++) {
        let address = randomPick(Object.keys(roadGraph));
        let place;
        do {
            place = randomPick(Object.keys(roadGraph));
            // 包裹的起點和終點不能夠是同一個地方
        } while (place == address);
        parcels.push({
            place,
            address
        });
    }
    return new VillageState("Post Office", parcels);
};

// runRobot(VillageState.random(), randomRobot);
// 版本一,步數不穩定

// runRobotAnimation(VillageState.random(), randomRobot); 
// 做者寫的動畫版本,至關直觀酷炫。。

// 第二個策略:事先指定一條能夠經過全部地點的路線
// 走兩遍就能夠確保投遞全部郵件
const mailRoute = [
    "Alice's House", "Cabin", "Alice's House", "Bob's House",
    "Town Hall", "Daria's House", "Ernie's House",
    "Grete's House", "Shop", "Grete's House", "Farm",
    "Marketplace", "Post Office"
];

// [a,b,c].slice(1) 
// → [b,c]
// [a,b,c].slice(1, 2) 
// → [b] // 包括start不包括end
function routeRobot(state, memory) {
    if(memory.length == 0) {
        memory = mailRoute;
    }
    // memory相似於隊列
    // 等價: return {direction: memory.shift(), memory: memory}
    return {
        direction: memory[0],
        memory: memory.slice(1)
    };
}

// runRobot(VillageState.random(), routeRobot, []);
// 版本二,最多26步

/**
 * The problem of finding a route 
 * through a graph is a typical search problem. 
 * We can tell whether a given solution (a route) 
 * is a valid solution, but we can’t directly compute
 * the solution the way we could for 2 + 2. 
 * Instead, we have to keep creating potential solutions 
 * until we find one that works.
 */

// 返回一點到另外一點的最短路線,參考:C++ 電路佈線/最短路徑問題
function findRoute(graph, from, to) {
    let work = [{at: from, route: []}]; // 其實也是個隊列
    for(let i = 0; i < work.length; i++) {
        let {at, route} = work[i]; // 原來還能夠這樣賦值。。
        for(let place of graph[at]) {
            // 搜索四周圍,若是找到了目的地就直接+1返回。
            if(place == to) return route.concat(place);
            if(!work.some(w => w.at == place)) { // 判斷點是否已經入隊
                work.push({at: place, route: route.concat(place)});
            }
        }
    }
}

function goalOrientedRobot({place, parcels}, route) {
    // 首先判斷當前制定的路線走完沒有,
    // 走完就從新制定下一條路線
    // 逐個包裹處理(固然也有可能順
    // 路完成其它包裹的fetch和投遞)
    if(route.length == 0) {
        let parcel = parcels[0];
        if(parcel.place != place) {
            // 制定取包裹路線
            route = findRoute(roadGraph, place, parcel.place);
        } else {
            // 制定投遞路線
            route = findRoute(roadGraph, place, parcel.address);
        }
    }
    return {direction: route[0], memory: route.slice(1)};
}


runRobot(VillageState.random(), goalOrientedRobot, []);
// 版本三,平均十來步的樣子

 

Exercises

① Measuring a robotdom

function testRobot(state, robot, memory) {
    for(let turn = 0;; turn++) {
        if(state.parcels.length == 0) {
            return turn;
            break;
        }
        let action = robot(state, memory);
        state = state.move(action.direction);
        memory = action.memory;
    }
}

function compareRobots(robot1, memory1, robot2, memory2) {
    let tasks = [];
    for (let i = 0; i != 100; ++i) {
        tasks.push(VillageState.random());
    }
    let total1 = 0, total2 = 0;
    for (let task of tasks) {
        total1 += testRobot(task, robot1, memory1);
        total2 += testRobot(task, robot2, memory2);
    }
    console.log(`average turns: robot1 ${total1 / 100}, robot2 ${total2 / 100}`);
}

compareRobots(routeRobot, [], goalOrientedRobot, []);
// → average turns: robot1 18.07, robot2 15.03

- - -- - - - - -- -- -- - - - - -- -          -- - - 函數

② Robot efficiencypost

沒作任何優化的窮舉,並且還用遞歸。。。6個以上包裹瀏覽器應該會直接崩潰掉。。fetch

/**
  max表示一個包裹要被處理的次數
  arr爲長度爲包裹數量的全0數組
  arr[i]表示第i個包裹被處理的次數
   當arr爲[max, max, max, ...]時
   表示全部包裹已經被處理完
   返回的sequences包含處理包裹的
   全部順序集合
 */
function makeSequences(max, arr) {
    
    const sequences = [];
    
    const fillArrWith = (max, arr, start, sequence) => {
        // 填充起始點
        arr[start] += 1;
        sequence.push(start);
        // 判斷是否已經填充滿
        let sum = 0;
        for (let x of arr) {
            sum += x;
        };
        if (sum == max * arr.length) {
            sequences.push(sequence);
            return;
        }
        // 尋找下一個填充點
        for (let i = 0; i != arr.length; ++i) {
            if (arr[i] < max) fillArrWith(max, arr.slice(), i, sequence.slice()); 
        }        
    };
    
    for (let i = 0; i != arr.length; ++i) {
        fillArrWith(max, arr.slice(), i, []);
    }    
    
    return sequences;
}



/**
   把生成的序列轉化爲具體業務相關的表達。
   獲得route並非實際的route 而是可能不相鄰的地點
  routes包含全部可以完成任務的路線
 */
function sequencesToRoutes(sequences, {place, parcels}) {
    const routes = [];
    
    const flag = parcels.map(() => 0); // 用於以後拷貝用
    // 逐個序列處理
    for (let sequence of sequences) {
        let route = [place]; // 添加起點
        let localFlag = flag.slice(); // 標記包裹的狀態
        for (let num of sequence) {
            if (localFlag[num] == 0) { // 第一次處理包裹num:到place取包裹
                localFlag[num]++; // 包裹num已取,這樣能夠保證某包裹的place必定優先於該包裹的address入隊
                if (route[route.length - 1] != parcels[num].place) { // 避免出現兩個連續重複place
                    route.push(parcels[num].place);
                }                
            } else { // 第二次處理包裹num: 送包裹,去該包裹的目的地
                if (route[route.length-1] != parcels[num].address) {
                    route.push(parcels[num].address);
                }                        
            }        
        }
        routes.push(route);
    }
    
    return routes;
}

/**
 * 計算單個路線須要的最短步數
 * turnsMap用於保存已經計算的兩點值,避免重複計算
 */
function turnsOfRoute(route, turnsMap=new Map()) {
    let totalTurns = 0;
    for (let i = 0; i != route.length - 1; ++i) {
        // 兩點、兩點處理。
        let routeKey = route[i].concat(route[i + 1]);
        let turns = turnsMap.get(routeKey);
        if (turns != undefined) {
            totalTurns += turns;
        } else {
            turns = findRoute(roadGraph, route[i], route[i + 1]).length;
            // 計算 a到b 的最小步數 ↑
            // 保存計算結果 ↓
            turnsMap.set(routeKey, turns);
            routeKey = route[i + 1].concat(route[i]); // a到b和b到a的最短路是同樣的。
            turnsMap.set(routeKey, turns);
            
            totalTurns += turns;
        }
    }
    return totalTurns;
}

/**
 * 尋找最短路線
 */
function shortestRoute(routes) {
    let min = Infinity;
    let tempRoute;
    let turnsMap = new Map(); // 用於保存已經計算的兩點值,避免重複計算
    for (let route of routes) {
        let turns = turnsOfRoute(route, turnsMap);
        if (turns < min) {
            min = turns;
            tempRoute = route; // 保存最短路線
        }
    }
    
    
    // 將最短路線轉化爲相鄰的能夠實際移動的地點序列
    let result = [];    
    for (let i = 0; i != tempRoute.length - 1; ++i) {
        // 仍然是兩點、兩點處理
        let midRoute = findRoute(roadGraph, tempRoute[i], tempRoute[i + 1]);
        if (result[result.length - 1] != midRoute[0]) { // 避免出現兩個連續重複place
            result = result.concat(midRoute);
        } else {
            result = result.concat(midRoute.shift());
        }
    }
    return result;
}

function simpleRobot({place, parcels}, route) {
    if(route.length == 0) {
        let sequences = makeSequences(2, parcels.map(() => 0)); // 轉化成一種比較抽象的東西。。。
        let routes = sequencesToRoutes(sequences, {place, parcels}); 
        route = shortestRoute(routes);
    }
    return {direction: route[0], memory: route.slice(1)};
}

//runRobot(VillageState.random(), simpleRobot, []);
// 版本四,窮舉。。 平均 10.64
//runRobotAnimation(VillageState.random(), simpleRobot, []);

- - -- - - - - -- -- -- - - - - -- -          -- - - 優化

③ Persistent group動畫

結果沒錯,實現上有點跑偏。。(英語閱讀能力不足形成的)ui

class PGroup {
    add(x) {
        let result = Object.create(PGroup.prototype);
        if (this.container == undefined) {
            this.container = [];
        }
        
        if (!this.container.includes(x)) {
            result.container = this.container.concat(x);    
          } else {
              result.container = this.container;
          }
        
        return result;
    }

    delete(x) {
        let result = Object.create(PGroup.prototype);
        if (this.container != undefined) {
            result.container = this.container.filter(a => !(a === x));    
        }
        return result;
    }

    has(x) {
        if (this.container != undefined) {
            return this.container.includes(x);
        }
        return false;
    }
}

PGroup.empty = Object.create(PGroup.prototype);

let a = PGroup.empty.add("a");
let ab = a.add("b");
let b = ab.delete("a");

console.log(b.has("b"));
// → true
console.log(a.has("b"));
// → false
console.log(b.has("a"));
// → false
相關文章
相關標籤/搜索