S SmartDocs
系列: Angelia java 611 行 · 更新于 2026-08-13

Mountain.java

Angelia/Java/Lab_3/code/Mountain.java

import java.util.Arrays;
import java.util.HashSet;

/**
 * Mountain.java —— Lab 3: Mountain Climbing(登山實驗)完整參考實作
 * 課程:COMP1110/1140/6710 — Structured Programming
 *
 * ─────────────────────────────────────────────
 * 情境說明
 * ─────────────────────────────────────────────
 * 你是一位在「數位山脈」中工作的測量員。
 * 地形用一張高度圖(heightmap)表示:一個 2D 整數陣列,
 * heightmap[y][x] 就是座標 (y, x) 的海拔高度。
 * 地圖中存在唯一的「黃金山頂」:全地圖的最高點。
 *
 * 本檔案依照講義完成三個活動:
 *   Activity 1: lookAround —— 觀察四周,回傳最高鄰居的方向
 *   Activity 2: climb      —— 從 [0][0] 一步一步爬到山頂
 *   Activity 3: fastTravel —— 用「矩形二分搜尋」快速找到山頂
 *
 * ─────────────────────────────────────────────
 * 編譯與執行(-ea 會啟用 assert,測試才會真正生效!)
 * ─────────────────────────────────────────────
 *   javac Direction.java Mountain.java
 *   java -ea Mountain
 */
public class Mountain {

    // ================================================================
    // 常數與共用輔助函式
    // ================================================================

    /**
     * 「界外」的高度值。
     *
     * 講義說:「地圖邊界之外的高度,可以當成 0,
     * 或某個非常低的數字。」
     * 我們選擇 Integer.MIN_VALUE(int 的最小值,約 -21 億),
     * 這樣即使地圖上真的出現 0 或負數高度,
     * 界外也永遠不可能被誤認成「最高的鄰居」。
     */
    static final int OUT_OF_BOUNDS = Integer.MIN_VALUE;

    /**
     * 安全地讀取座標 (y, x) 的高度;
     * 若座標超出地圖範圍,回傳 OUT_OF_BOUNDS。
     *
     * 這個小函式是本作業「邊界處理」的核心技巧:
     * 與其在 lookAround 裡寫四段重複的 if 邊界判斷,
     * 不如把「安全讀取」包成一個函式,讓主要邏輯保持乾淨。
     * 這種把麻煩細節包起來的做法,叫做「抽象化」(abstraction)。
     *
     * 前置條件:heightmap 是非空的矩形陣列(見 isRectangular)。
     * 後置條件:
     *   - (y, x) 在地圖內 → 回傳 heightmap[y][x]
     *   - (y, x) 在地圖外 → 回傳 OUT_OF_BOUNDS
     */
    static int heightAt(int y, int x, int[][] heightmap) {
        // y 必須是合法的「列」索引:0 <= y < 列數
        boolean insideRows = 0 <= y && y < heightmap.length;
        // x 必須是合法的「行」索引:0 <= x < 行數
        boolean insideCols = 0 <= x && x < heightmap[0].length;

        if (!insideRows || !insideCols) {
            return OUT_OF_BOUNDS;   // 界外:回傳「超級低」的高度
        }
        return heightmap[y][x];     // 界內:正常讀取
    }

    /**
     * 檢查 heightmap 是否為「非空的矩形」2D 陣列。
     *
     * Java 的 2D 陣列其實是「陣列的陣列」,
     * 每一列的長度可以不同(稱為 ragged array,鋸齒狀陣列),
     * 所以「是不是矩形」需要自己檢查。
     * 這個函式只拿來寫前置條件的 assert 用。
     */
    static boolean isRectangular(int[][] map) {
        if (map == null || map.length == 0) return false;      // 沒有任何列
        if (map[0] == null || map[0].length == 0) return false; // 第一列是空的
        for (int[] row : map) {
            // 每一列都必須存在,而且長度要跟第一列一樣
            if (row == null || row.length != map[0].length) return false;
        }
        return true;
    }

    // ================================================================
    // Activity 1: lookAround —— 觀察四周
    // ================================================================

    /**
     * 觀察座標 (y, x) 的四個鄰居,決定該往哪個方向走。
     *
     * 規則:
     *   - 比較北、東、南、西四個鄰居的高度,
     *     回傳「最高鄰居」所在的方向。
     *   - 如果目前的格子比四個鄰居都「嚴格更高」,
     *     回傳 Direction.PEAK(你就是山頂)。
     *   - 超出地圖邊界的鄰居視為高度 OUT_OF_BOUNDS(超級低),
     *     所以永遠不會勝出。
     *
     * 打平規則(tie-breaking):
     *   若有多個鄰居同為最高,依 北 → 東 → 南 → 西 的順序取第一個。
     *   (Mountain Lab 產生的有效地圖,沿途不會出現這種情形,
     *   但寫程式時仍要讓行為「明確、可預測」。)
     *
     * 前置條件(precondition):
     *   1. heightmap 是非空的矩形 2D 陣列。
     *   2. 0 <= y < 列數,0 <= x < 行數(座標必須在地圖內)。
     *
     * 後置條件(postcondition):
     *   - 回傳 PEAK 時:heightmap[y][x] 嚴格大於四個鄰居的高度。
     *   - 回傳其他方向時:該方向鄰居的高度是四個鄰居中的最大值,
     *     且目前格子的高度 <= 該鄰居的高度
     *     (也就是:目前格子並沒有嚴格高於所有鄰居)。
     */
    static Direction lookAround(int y, int x, int[][] heightmap) {
        // ---- 前置條件檢查(執行時要加 -ea 才會生效)----
        assert isRectangular(heightmap)
                : "前置條件違反:heightmap 必須是非空的矩形陣列";
        assert 0 <= y && y < heightmap.length
                : "前置條件違反:y=" + y + " 超出列範圍";
        assert 0 <= x && x < heightmap[0].length
                : "前置條件違反:x=" + x + " 超出行範圍";

        // ---- 讀取自己與四個鄰居的高度 ----
        int current = heightmap[y][x];                 // 腳下的高度
        int north   = heightAt(y - 1, x, heightmap);   // 北:上一列
        int east    = heightAt(y, x + 1, heightmap);   // 東:右一行
        int south   = heightAt(y + 1, x, heightmap);   // 南:下一列
        int west    = heightAt(y, x - 1, heightmap);   // 西:左一行

        // ---- 找出四個鄰居中的最大高度 ----
        // Math.max 一次只能比兩個數,所以用巢狀呼叫比出四個的最大值。
        int best = Math.max(Math.max(north, east), Math.max(south, west));

        // ---- 判斷結果 ----
        if (current > best) {
            return Direction.PEAK;      // 我比所有鄰居都高:山頂!
        }
        // 依「北 → 東 → 南 → 西」的優先順序,回傳第一個最高的鄰居。
        if (north == best) return Direction.NORTH;
        if (east == best)  return Direction.EAST;
        if (south == best) return Direction.SOUTH;
        return Direction.WEST;          // 剩下的只可能是西
    }

    // ================================================================
    // Activity 2: climb —— 一步一步爬到山頂
    // ================================================================

    /** 僅供測試/示範用:記錄最近一次 climb 花了幾步。 */
    static int lastClimbSteps = -1;

    /**
     * 從 [0][0] 出發,每一步都走向「最高的鄰居」,
     * 直到 lookAround 回報 PEAK 為止。這種策略叫做
     * 「貪婪爬坡法」(greedy hill climbing)。
     *
     * 為什麼這樣一定會到達山頂?
     * 因為 Mountain Lab 產生的地圖保證:
     * 「局部最佳就是全域最佳」——lookAround 給的方向
     * 永遠讓你嚴格更接近山頂,絕不會把你帶遠。
     * 每走一步,腳下的高度嚴格上升,所以不可能繞圈子,
     * 也不可能走回頭路,最後一定停在山頂。
     *
     * 前置條件:
     *   heightmap 是非空的矩形 2D 陣列,
     *   且為「有效地圖」:唯一山頂、沿途方向必定指向山頂。
     * 後置條件:
     *   回傳長度為 2 的新陣列 {y, x}:山頂的座標,
     *   且 lookAround(y, x, heightmap) == PEAK。
     *
     * @return 山頂座標 {y, x}
     */
    static int[] climb(int[][] heightmap) {
        // ---- 前置條件 ----
        assert isRectangular(heightmap)
                : "前置條件違反:heightmap 必須是非空的矩形陣列";

        int y = 0;          // 目前位置:從左上角出發
        int x = 0;
        int steps = 0;      // 已經走了幾步

        // 先看看四周;只要還不是山頂,就繼續走
        Direction dir = lookAround(y, x, heightmap);
        while (dir != Direction.PEAK) {
            // 依照方向更新座標。
            // 這裡用 Java 14+ 的「箭頭 switch」:
            // 每個分支互相獨立,不會像傳統 switch 忘了 break 而「掉落」。
            switch (dir) {
                case NORTH -> y--;   // 北:列索引減 1(往上)
                case EAST  -> x++;   // 東:行索引加 1(往右)
                case SOUTH -> y++;   // 南:列索引加 1(往下)
                case WEST  -> x--;   // 西:行索引減 1(往左)
                case PEAK  -> { }    // 不可能到這裡(while 已排除)
            }
            steps++;

            // 防禦性檢查:有效地圖上,路徑高度嚴格上升,
            // 走的步數絕不可能超過格子總數。
            // 若超過,代表地圖無效(例如有高原),與其無窮迴圈,
            // 不如讓 assert 立刻失敗、告訴我們哪裡出問題。
            assert steps <= heightmap.length * heightmap[0].length
                    : "步數超過格子總數:這張地圖大概不是有效的單峰地圖";

            dir = lookAround(y, x, heightmap);  // 到新位置再看一次四周
        }

        // 依講義要求:印出總步數
        System.out.println("'climb' found the peak in " + steps + " steps!");
        lastClimbSteps = steps;   // 記下來,讓 main 的測試可以驗證

        // ---- 後置條件:回傳的座標必須真的是山頂 ----
        assert lookAround(y, x, heightmap) == Direction.PEAK
                : "後置條件違反:climb 停下來的位置不是山頂";

        return new int[]{y, x};   // 打包成長度 2 的新陣列 {y, x}
    }

    // ================================================================
    // Activity 3: fastTravel —— 矩形二分搜尋
    // ================================================================

    /** 僅供測試/示範用:記錄最近一次 fastTravel 探測了幾次。 */
    static int lastFastTravelSteps = -1;

    /**
     * 用「2D 版的二分搜尋」快速找到山頂。
     *
     * 核心想法:
     *   我們隨時維護一個「山頂必定在裡面」的矩形搜尋範圍
     *   [top..bottom] × [left..right](含邊界)。
     *   每一輪:
     *     1. 取矩形的中心點 (midY, midX),呼叫 lookAround。
     *     2. 若回傳 PEAK:直接找到山頂,結束。
     *     3. 否則,方向告訴我們山頂在中心點的哪一側:
     *        - NORTH → 山頂的列 <  midY,砍掉下半部(bottom = midY - 1)
     *        - SOUTH → 山頂的列 >  midY,砍掉上半部(top    = midY + 1)
     *        - EAST  → 山頂的行 >  midX,砍掉左半部(left   = midX + 1)
     *        - WEST  → 山頂的行 <  midX,砍掉右半部(right  = midX - 1)
     *   每一輪都把矩形的某一邊縮小約一半,
     *   所以步數是 O(log m + log n),遠小於 climb 的 O(m + n)。
     *
     * 為什麼「方向」可以拿來砍範圍?
     *   有效地圖保證 lookAround 的方向讓你「嚴格更接近山頂」。
     *   如果往北走會更接近,山頂的列號必定小於現在的列號——
     *   所以山頂絕不可能在中心點那一列或更南邊,可以安心砍掉。
     *
     * 前置條件:同 climb。
     * 後置條件:同 climb —— 回傳山頂座標 {y, x}。
     *
     * @return 山頂座標 {y, x}
     */
    static int[] fastTravel(int[][] heightmap) {
        // ---- 前置條件 ----
        assert isRectangular(heightmap)
                : "前置條件違反:heightmap 必須是非空的矩形陣列";

        // 搜尋範圍:一開始是整張地圖(注意都是「含」邊界的索引)
        int top    = 0;                          // 最上面可能的列
        int bottom = heightmap.length - 1;       // 最下面可能的列
        int left   = 0;                          // 最左邊可能的行
        int right  = heightmap[0].length - 1;    // 最右邊可能的行

        int steps = 0;   // 探測(probe)了幾次

        while (true) {
            // 取搜尋矩形的中心點(整數除法自動向下取整)
            int midY = (top + bottom) / 2;
            int midX = (left + right) / 2;

            steps++;
            Direction dir = lookAround(midY, midX, heightmap);

            if (dir == Direction.PEAK) {
                // 依講義要求:印出總步數
                System.out.println(
                        "'fastTravel' found the peak in " + steps + " steps!");
                lastFastTravelSteps = steps;
                return new int[]{midY, midX};
            }

            // 根據方向,砍掉「山頂不可能在」的那半邊
            switch (dir) {
                case NORTH -> bottom = midY - 1;  // 山頂在更北:往上縮
                case SOUTH -> top    = midY + 1;  // 山頂在更南:往下縮
                case EAST  -> left   = midX + 1;  // 山頂在更東:往右縮
                case WEST  -> right  = midX - 1;  // 山頂在更西:往左縮
                case PEAK  -> { }                 // 不可能(上面已 return)
            }

            // 迴圈不變量(loop invariant):
            // 山頂永遠在 [top..bottom] × [left..right] 這個矩形裡。
            // 若矩形變成空的,代表地圖違反了前置條件。
            assert top <= bottom && left <= right
                    : "搜尋矩形變成空的:這張地圖大概不是有效的單峰地圖";
        }
    }

    // ================================================================
    // 測試輔助工具
    // ================================================================

    /**
     * 產生一張「錐形」測試地圖:
     * 以 (peakY, peakX) 為山頂,高度隨距離遞減——
     *   高度 = base - 3 * |y - peakY| - 2 * |x - peakX|
     *
     * 兩個方向刻意用「不同」的權重(3 與 2),
     * 這樣任何一格的四個鄰居永遠不會同高(不會打平),
     * 每一格的正確答案就唯一,方便寫測試。
     *
     * 這種地圖是有效的單峰地圖:
     * 任何位置往最高鄰居走,都嚴格更接近山頂。
     */
    static int[][] coneMap(int rows, int cols, int peakY, int peakX) {
        int[][] map = new int[rows][cols];
        int base = 3 * rows + 2 * cols;   // 讓所有高度都是正數
        for (int y = 0; y < rows; y++) {
            for (int x = 0; x < cols; x++) {
                map[y][x] = base - 3 * Math.abs(y - peakY)
                                 - 2 * Math.abs(x - peakX);
            }
        }
        return map;
    }

    /**
     * 錐形地圖上,座標 (y, x) 的「理論正確答案」。
     *
     * 因為列方向權重(3)大於行方向權重(2):
     *   - 只要列還沒對齊山頂(y != peakY),
     *     南北方向的高度差 ±3 一定壓過東西的 ±2,
     *     所以答案必是 NORTH 或 SOUTH。
     *   - 列對齊之後才輪到東西方向。
     *   - 兩者都對齊,就是 PEAK。
     * 有了這個「封閉公式」,我們就能窮舉地圖上每一格,
     * 把 lookAround 的輸出跟理論答案逐一比對。
     */
    static Direction expectedOnCone(int y, int x, int peakY, int peakX) {
        if (y == peakY && x == peakX) return Direction.PEAK;
        if (y != peakY) {
            return (peakY < y) ? Direction.NORTH : Direction.SOUTH;
        }
        return (peakX > x) ? Direction.EAST : Direction.WEST;
    }

    /** 計算 ceil(log2(n)),用來檢驗 fastTravel 的步數上限。 */
    static int ceilLog2(int n) {
        if (n <= 1) return 0;
        // numberOfLeadingZeros:int 的 32 位元中,開頭有幾個 0。
        // 32 - nlz(n - 1) 剛好就是 ceil(log2(n))。
        return 32 - Integer.numberOfLeadingZeros(n - 1);
    }

    // ================================================================
    // 測試(全部用 assert;請以 java -ea Mountain 執行)
    // ================================================================

    /** 測試 1:講義裡的 3×3 範例地圖。 */
    static void testLookAroundOnLabExample() {
        int[][] smallMap = {
                {10, 20, 10},   // Row 0
                {20, 50, 20},   // Row 1
                {10, 20, 10}    // Row 2
        };

        // 中央 50 比四周(20,20,20,20)都高 → PEAK
        assert lookAround(1, 1, smallMap) == Direction.PEAK;

        // 四個「邊」的中點:最高鄰居都是中央的 50
        assert lookAround(0, 1, smallMap) == Direction.SOUTH; // 下方 50 最高
        assert lookAround(1, 0, smallMap) == Direction.EAST;  // 右方 50 最高
        assert lookAround(2, 1, smallMap) == Direction.NORTH; // 上方 50 最高
        assert lookAround(1, 2, smallMap) == Direction.WEST;  // 左方 50 最高

        // 角落 (0,0):東(20)與南(20)同高「打平」。
        // 依我們文件化的打平規則(北→東→南→西),應回傳 EAST。
        assert lookAround(0, 0, smallMap) == Direction.EAST;

        System.out.println("[1] lookAround:3x3 講義範例地圖 ... 通過");
    }

    /** 測試 2:手工設計的 3×4 地圖,全部 12 格逐一驗證(完整分支覆蓋)。 */
    static void testLookAroundFullBranchCoverage() {
        // 山頂在 (1, 2) 的 90。所有相鄰高度都不同,答案唯一。
        int[][] map = {
                { 5, 30, 40, 35},   // Row 0
                {10, 50, 90, 60},   // Row 1
                { 8, 20, 45, 30}    // Row 2
        };

        // ---- Row 0(上邊界:北邊是界外)----
        assert lookAround(0, 0, map) == Direction.EAST;  // 角落:東30 > 南10
        assert lookAround(0, 1, map) == Direction.SOUTH; // 南50 最高
        assert lookAround(0, 2, map) == Direction.SOUTH; // 南90 最高
        assert lookAround(0, 3, map) == Direction.SOUTH; // 角落:南60 > 西40

        // ---- Row 1 ----
        assert lookAround(1, 0, map) == Direction.EAST;  // 左邊界:東50 最高
        assert lookAround(1, 1, map) == Direction.EAST;  // 東90 最高
        assert lookAround(1, 2, map) == Direction.PEAK;  // 山頂本人!
        assert lookAround(1, 3, map) == Direction.WEST;  // 右邊界:西90 最高

        // ---- Row 2(下邊界:南邊是界外)----
        assert lookAround(2, 0, map) == Direction.EAST;  // 角落:東20 > 北10
        assert lookAround(2, 1, map) == Direction.NORTH; // 北50 最高
        assert lookAround(2, 2, map) == Direction.NORTH; // 北90 最高
        assert lookAround(2, 3, map) == Direction.NORTH; // 角落:北60 > 西45

        // 五種 Direction 值(含 PEAK)都至少被回傳一次 → 完整分支覆蓋
        System.out.println("[2] lookAround:3x4 地圖 12 格全驗證(分支覆蓋)... 通過");
    }

    /** 測試 3:退化地圖——1×1、單列、單行。邊界處理最容易出錯的地方。 */
    static void testLookAroundDegenerateMaps() {
        // 1×1 地圖:四個鄰居全是界外 → 自己就是山頂
        int[][] tiny = {{42}};
        assert lookAround(0, 0, tiny) == Direction.PEAK;

        // 單列地圖(1×5):只能往東西走
        int[][] oneRow = {{10, 20, 30, 40, 25}};
        assert lookAround(0, 0, oneRow) == Direction.EAST;  // 20 在右邊
        assert lookAround(0, 3, oneRow) == Direction.PEAK;  // 40 是山頂
        assert lookAround(0, 4, oneRow) == Direction.WEST;  // 40 在左邊

        // 單行地圖(5×1):只能往南北走
        int[][] oneCol = {{10}, {20}, {30}, {40}, {25}};
        assert lookAround(0, 0, oneCol) == Direction.SOUTH; // 20 在下面
        assert lookAround(3, 0, oneCol) == Direction.PEAK;  // 40 是山頂
        assert lookAround(4, 0, oneCol) == Direction.NORTH; // 40 在上面

        System.out.println("[3] lookAround:1x1/單列/單行 退化地圖 ... 通過");
    }

    /** 測試 4:在多張錐形地圖上「窮舉」每一格,逐一比對理論答案。 */
    static void testLookAroundExhaustivelyOnConeMaps() {
        int[][] shapes = {{1, 1}, {1, 8}, {8, 1}, {5, 5}, {9, 7}, {12, 24}};
        int checked = 0;

        for (int[] shape : shapes) {
            int rows = shape[0];
            int cols = shape[1];
            // 讓山頂輪流出現在地圖的「每一個」位置
            for (int peakY = 0; peakY < rows; peakY++) {
                for (int peakX = 0; peakX < cols; peakX++) {
                    int[][] map = coneMap(rows, cols, peakY, peakX);
                    // 再對地圖上的「每一格」比對答案
                    for (int y = 0; y < rows; y++) {
                        for (int x = 0; x < cols; x++) {
                            Direction got  = lookAround(y, x, map);
                            Direction want = expectedOnCone(y, x, peakY, peakX);
                            assert got == want
                                    : "在 " + rows + "x" + cols + " 地圖(山頂 ("
                                    + peakY + "," + peakX + "))的座標 ("
                                    + y + "," + x + "):期望 " + want
                                    + ",卻得到 " + got;
                            checked++;
                        }
                    }
                }
            }
        }
        System.out.println("[4] lookAround:錐形地圖窮舉測試,共比對 "
                + checked + " 個座標 ... 通過");
    }

    /** 測試 5:climb 的手工小地圖測試(山頂在不同角落)。 */
    static void testClimbOnHandCraftedMaps() {
        System.out.println("[5] climb:手工地圖測試");

        // (a) 山頂就在出發點 [0][0]:應該 0 步完成
        int[][] cornerPeak = {
                {99, 80, 60},
                {85, 70, 50},
                {62, 55, 40}
        };
        int[] p1 = climb(cornerPeak);
        assert Arrays.equals(p1, new int[]{0, 0})
                : "山頂應在 (0,0),卻回傳 " + Arrays.toString(p1);
        assert lastClimbSteps == 0 : "從山頂出發應該走 0 步";

        // (b) 山頂在對角(右下角):距離出發點最遠
        int[][] farPeak = {
                { 5, 12, 20},
                {14, 30, 44},
                {22, 48, 77}
        };
        int[] p2 = climb(farPeak);
        assert Arrays.equals(p2, new int[]{2, 2})
                : "山頂應在 (2,2),卻回傳 " + Arrays.toString(p2);
        // 有效地圖上每一步都嚴格接近山頂,
        // 所以步數 = 曼哈頓距離 |2-0| + |2-0| = 4
        assert lastClimbSteps == 4 : "步數應為 4,卻是 " + lastClimbSteps;

        // (c) 講義的 3×3 範例:山頂在正中央
        int[][] smallMap = {
                {10, 20, 10},
                {20, 50, 20},
                {10, 20, 10}
        };
        int[] p3 = climb(smallMap);
        assert Arrays.equals(p3, new int[]{1, 1})
                : "山頂應在 (1,1),卻回傳 " + Arrays.toString(p3);
        assert lastClimbSteps == 2 : "步數應為 2,卻是 " + lastClimbSteps;

        System.out.println("    手工地圖全部通過");
    }

    /** 測試 6:climb 與 fastTravel 在各種尺寸、各種山頂位置的錐形地圖上。 */
    static void testClimbAndFastTravelOnConeMaps() {
        System.out.println("[6] climb + fastTravel:錐形地圖系統化測試");

        int[][] shapes = {{1, 1}, {1, 9}, {9, 1}, {5, 8}, {24, 12}};

        for (int[] shape : shapes) {
            int rows = shape[0];
            int cols = shape[1];

            // 測試的山頂位置:四個角落 + 正中央(用 HashSet 去掉重複)
            int[][] peaks = {
                    {0, 0}, {0, cols - 1},
                    {rows - 1, 0}, {rows - 1, cols - 1},
                    {rows / 2, cols / 2}
            };
            HashSet<String> seen = new HashSet<>();

            for (int[] pk : peaks) {
                int peakY = pk[0];
                int peakX = pk[1];
                if (!seen.add(peakY + "," + peakX)) continue; // 跳過重複位置

                int[][] map = coneMap(rows, cols, peakY, peakX);
                System.out.println("    地圖 " + rows + "x" + cols
                        + ",山頂在 (" + peakY + "," + peakX + "):");

                // ---- climb ----
                System.out.print("      ");
                int[] got = climb(map);
                assert Arrays.equals(got, new int[]{peakY, peakX})
                        : "climb 找錯山頂:" + Arrays.toString(got);
                // 步數應等於從 (0,0) 到山頂的曼哈頓距離
                assert lastClimbSteps == peakY + peakX
                        : "climb 步數應為 " + (peakY + peakX)
                        + ",卻是 " + lastClimbSteps;

                // ---- fastTravel ----
                System.out.print("      ");
                int[] got2 = fastTravel(map);
                assert Arrays.equals(got2, new int[]{peakY, peakX})
                        : "fastTravel 找錯山頂:" + Arrays.toString(got2);
                // 步數上限:每次探測至少砍掉某一邊的一半
                int bound = ceilLog2(rows) + ceilLog2(cols) + 2;
                assert lastFastTravelSteps <= bound
                        : "fastTravel 步數 " + lastFastTravelSteps
                        + " 超過理論上限 " + bound;

                // ---- 兩種方法必須找到同一個山頂 ----
                assert Arrays.equals(got, got2)
                        : "climb 與 fastTravel 的答案不一致!";
            }
        }
        System.out.println("    系統化測試全部通過");
    }

    /** 示範:在大地圖上比較 climb 與 fastTravel 的步數差距。 */
    static void demoSpeedComparison() {
        System.out.println("[7] 步數比較:128x128 地圖,山頂在 (100, 111)");
        int[][] big = coneMap(128, 128, 100, 111);

        System.out.print("    ");
        int[] a = climb(big);        // 預期 100 + 111 = 211 步
        System.out.print("    ");
        int[] b = fastTravel(big);   // 預期最多 7 + 7 + 2 = 16 次探測

        assert Arrays.equals(a, b);
        assert lastClimbSteps == 211;
        assert lastFastTravelSteps <= 16;
        System.out.println("    同一座山,climb 走了 " + lastClimbSteps
                + " 步;fastTravel 只探測 " + lastFastTravelSteps + " 次!");
    }

    // ================================================================
    // main:依序執行所有測試
    // ================================================================

    public static void main(String[] args) {
        // 小技巧:故意利用 assert 的「副作用」偵測 -ea 是否開啟。
        // 若 assert 沒啟用,這行不會執行,assertionsEnabled 維持 false。
        boolean assertionsEnabled = false;
        assert assertionsEnabled = true;   // 啟用時才會把它設成 true
        if (!assertionsEnabled) {
            System.out.println("【警告】assert 未啟用!以下測試不會真正檢查。");
            System.out.println("        請改用:java -ea Mountain");
        }

        System.out.println("=== Lab 3: Mountain Climbing — 完整測試 ===");
        testLookAroundOnLabExample();          // Activity 1
        testLookAroundFullBranchCoverage();    // Activity 1
        testLookAroundDegenerateMaps();        // Activity 1
        testLookAroundExhaustivelyOnConeMaps();// Activity 1
        testClimbOnHandCraftedMaps();          // Activity 2
        testClimbAndFastTravelOnConeMaps();    // Activity 2 + 3
        demoSpeedComparison();                 // Activity 2 vs 3

        System.out.println();
        System.out.println("=== 全部測試通過! ===");
    }
}

相关文章