LMU ☀️ CMSI 3300
ARTIFICIAL INTELLIGENCE
HOMEWORK #1 PARTIAL ANSWERS

Answers to textbook problems are not online.

Sentry Functional Spec

Answers vary, but as you know, functional specifications have to meet some minimal standards to be "functional specifications."

Paper on Cognition

In your essay, I'll be looking for whether you are consistent in your answer. Either you have to hammer away at the idea that robotics, speech recognition and vision have suffered greatly from everyone focusing on problem solving and knowledge representation, or at the idea that the thinking part of AI is completely indispensible since without it, we'd be stuck with pure reflex agents, which we don't consider intelligent.

PEAS

Music Composer

  Performance Measures - number of measures composed per unit time,
     number of instruments considered, ease of play by a human,
     range of frequencies within human audible zone,
     melodic, harmonic and rhythmic criteria, ...

  Environment
    Software

  Actuators
    None required, this can be a pure softbot

  Sensors
    Code that reads in basic parameters

Aircraft Autolander

  Performance Measures
    Lack of damage to plane, other aircraft or ground structures,
    lack of injuries to passengers or ground crew or other innocent
    observers, cargo remains intact, fuel economy, lands at correct airport
    on correct runway, doesn't take too long

  Environment
    Lower atmosphere and surface of planet Earth.

  Actuators
    Throttle, landing gear, rudders, ailerons, flaps ...

  Sensors
    Cameras, Altimeter, Spedometer, other meters, ...

Essay Evaluator

  Performance Measures
    awards scores for quality, penalizes crap, detection of plaigarism,
    impartiality, usefulness of explanation of grading, ...

  Environment
    Software

  Actuators
    None, this can be a pure softbot

  Sensors
    File reading software, (perhaps even OCR)

Robotic Sentry Gun for the Keck Lab

  Performance Measures
    Percentage of correct targets hit, lack of hitting friends, minimal
    energy consumption, ...

  Environment
    The Keck Lab

  Actuators
    Gun, trigger, motors, camera, ...

  Sensors
    Camera, sonar, bump sensors, ...

Reflex Vacuum Agent

By "pure" reflex agent we mean the agent acts on the basis of the current
percept only, and ignores the rest of the percept history.  By rational
we mean the agent tries to maximize the performance measure.  What are the
maxiums?

If the world starts with two dirty squares the highest score the agent could
get is 3.  If it starts with one clean and one dirty square then the maximum
achievable score for the agent is 2 if it starts in the dirty
square and 1 if it starts in the clean one.  If both squares
start off clean, the best the agent can do is get 0 points.
To be rational the agent needs a function which does the best it
can over all situations.  We don't know the probability distribution
of starting states, so let's assume all are equally likely.

How many possible agent functions are there for pure reflex agents?
There are only four possible percepts and four possible actions
(left, right, suck, nop), so there are 4^4 = 256 possible functions.
We could take each of these possible functions and compute their
scores on each of the four possible initial environments and
see which function does the best when averaged over all of them.

Instead of trying all 1024 combinations, we can rule out a few
to begin with using our own brains.  First, we can replace all
clauses in which we try to move right in B or left in A with nops.
Second, we cannot allow both (A, clean) and (B, clean) to both result
in moves, or else the vacuum could run forever and rack up a score
approaching negative infinity.  Third, failing to suck in a dirty
square is definitely a point loser, so we have to keep these.
This pretty much leaves us with four possible functions.  Two of
them are:

    fn (A, clean) => right
     | (A, dirty) => suck
     | (B, clean) => nop
     | (B, dirty) => suck

and

    fn (A, clean) => nop
     | (A, dirty) => suck
     | (B, clean) => left
     | (B, dirty) => suck

Either one will get the 3 if both squares are dirty, but they may
miss a chance for a 1 (settling for a zero), and may have to
take a -1 when a 0 is possible.

The other two are:

    fn (A, clean) => nop
     | (A, dirty) => suck
     | (B, clean) => nop
     | (B, dirty) => suck

and

    fn (A, clean) => nop
     | (A, dirty) => suck
     | (B, clean) => nop
     | (B, dirty) => suck

These never get the 3 (they max at 2), they might miss the chance
for a 1 (settling for a zero, like the other functions), but they
never take a -1 in place of a zero.

Let's try a serious analysis.  We can treat only one representative
from each of the two groups, since within each group the functions
are mirror images.

Let f(A,clean)=right|f(x,dirty)=suck|f(B,clean)=nop
Let g(x,clean)=nop|g(x,dirty)=suck
Let states be represented as current_location x A.status x B.status

For f:

State              score f()  max theoretical score
---------------------------------------------------
(A, clean, clean)     -1      0
(A, clean, dirty)      1      1
(A, dirty, clean)      1      2
(A, dirty, dirty)      3      3
(B, clean, clean)      0      0
(B, clean, dirty)      2      2
(B, dirty, clean)      0      1
(B, dirty, dirty)      2      3

That's 8/12.  Now for g:

State              score f()  max theoretical score
---------------------------------------------------
(A, clean, clean)      0      0
(A, clean, dirty)      0      1
(A, dirty, clean)      2      2
(A, dirty, dirty)      2      3
(B, clean, clean)      0      0
(B, clean, dirty)      2      2
(B, dirty, clean)      0      1
(B, dirty, dirty)      2      3

Also 8/12.  Well what do you know?  All four answers
are acceptable!

And guess what?  Sucking on a clean square doesn't hurt
either!  This performance measure was under-specified.  Good
to know, though.

Problem Interface

Problem.java
/**
 * A simple problem interface.
 */
public interface Problem<State, Action> {

    /**
     * Returns the array of actions applicable to the given state.
     */
    java.util.List<Action> actionsFor(State state);

    /**
     * Returns the state you get from applying the given action to the given state.
     */
    State go(State state, Action action);

    /**
     * Returns whether the given state is a goal.
     */
    boolean isGoal(State state);
}

Eight Puzzle

EightPuzzle.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Formatter;
import java.util.List;
import java.util.Random;

/**
 * The famous eight puzzle problem.
 */
public class EightPuzzle implements Problem<EightPuzzle.Board, EightPuzzle.Action> {

    public static enum Action {
        UP, DOWN, LEFT, RIGHT
    }

    /**
     * An 8-puzzle board.
     */
    public static class Board {

        /**
         * Holds the value of the tile at the given index position. The squares of the
         * board are numbered 0..8 as follows:
         * 
         * <pre>
         *     0  1  2
         *     3  4  5
         *     6  7  8
         * </pre>
         */
        private int[] tiles = new int[9];

        /**
         * Constructs a random board.
         */
        public Board() {
            tiles = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
            Random random = new Random();
            do {
                for (int i = 0; i < tiles.length; i++) {
                    int j = random.nextInt(tiles.length);
                    int x = tiles[i];
                    tiles[i] = tiles[j];
                    tiles[j] = x;
                }
            } while (!isSolvable());
        }

        /**
         * Creates a board with the given tile configuration.
         */
        public Board(int[] tiles) {
            System.arraycopy(tiles, 0, this.tiles, 0, tiles.length);
        }

        /**
         * Returns whether the given tile configuration is solvable. It's a well-known
         * theorem that an eight puzzle is solvable if the number of inversions is even.
         */
        public boolean isSolvable() {
            boolean solvable = true;
            for (int i = 0; i < tiles.length; i++) {
                if (tiles[i] == 0)
                    continue;
                for (int j = i + 1; j < tiles.length; j++) {
                    if (tiles[j] == 0)
                        continue;
                    if (tiles[i] > tiles[j])
                        solvable = !solvable;
                }
            }
            return solvable;
        }

        /**
         * Returns the sum of the manhattan distances for each tile.
         */
        public int manhattan() {
            int result = 0;
            for (int i = 0; i < 9; i++) {
                result += Math.abs(tiles[i] / 3 - i / 3) + Math.abs(tiles[i] % 3 - i % 3);
            }
            return result;
        }

        /**
         * Returns a simple textual representation of the board.
         */
        public String toString() {
            return new Formatter().format("%d%d%d\n%d%d%d\n%d%d%d", tiles[0], tiles[1], tiles[2], tiles[3], tiles[4],
                    tiles[5], tiles[6], tiles[7], tiles[8]).out().toString();
        }

        /**
         * Overrides Object.equals.
         */
        public boolean equals(Object o) {
            return o instanceof Board && Arrays.equals(tiles, ((Board) o).tiles);
        }

        /**
         * Overrides Object.hashCode.
         */
        public int hashCode() {
            return Arrays.hashCode(tiles);
        }
    }

    /**
     * Return the actions allowed from a given board state.
     */
    public List<Action> actionsFor(Board board) {
        List<Action> legalActions = new ArrayList<Action>();
        int blankPosition = 0;
        while (board.tiles[blankPosition] != 0)
            blankPosition++;
        if (blankPosition > 2)
            legalActions.add(Action.UP);
        if (blankPosition % 3 != 0)
            legalActions.add(Action.LEFT);
        if (blankPosition % 3 != 2)
            legalActions.add(Action.RIGHT);
        if (blankPosition < 6)
            legalActions.add(Action.DOWN);
        return legalActions;
    }

    /**
     * Returns the board you would get if you applied the given action to the given
     * board.
     */
    public Board go(Board board, Action action) {
        int offset = 0;
        switch (action) {
        case UP:
            offset = -3;
            break;
        case DOWN:
            offset = 3;
            break;
        case RIGHT:
            offset = 1;
            break;
        case LEFT:
            offset = -1;
            break;
        }
        int blankPosition = 0;
        while (board.tiles[blankPosition] != 0)
            blankPosition++;
        int newBlankPosition = blankPosition + offset;
        Board result = new Board(board.tiles);
        result.tiles[blankPosition] = board.tiles[newBlankPosition];
        result.tiles[newBlankPosition] = 0;
        return result;
    }

    /**
     * Returns whether the blank is in the lower right corner and tiles 1 through 8
     * appear in order from left to right, top to bottom.
     */
    public boolean isGoal(Board board) {
        return Arrays.equals(board.tiles, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 });
    }
}

Eight Puzzle Test Case

EightPuzzleTestCase.java
import java.util.Arrays;

import junit.framework.TestCase;
import edu.lmu.cs.search.EightPuzzle.Action;
import edu.lmu.cs.search.EightPuzzle.Board;

/**
 * JUnit test case for the 8-puzzle problem. Not very complete.
 */
public class EightPuzzleTestCase extends TestCase {

    EightPuzzle puzzle = new EightPuzzle();
    Board b1 = new Board(new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 });
    Board b2 = new Board(new int[] { 0, 1, 2, 3, 4, 5, 6, 8, 7 });
    Board b3 = new Board(new int[] { 1, 0, 2, 3, 4, 5, 6, 7, 8 });
    Board b4 = new Board(new int[] { 4, 3, 5, 6, 2, 7, 0, 8, 1 });
    Board b5 = new Board(new int[] { 4, 3, 5, 6, 2, 7, 0, 8, 1 });
    Board b6 = new Board(new int[] { 4, 3, 5, 0, 2, 7, 6, 8, 1 });
    Board b7 = new Board(new int[] { 4, 3, 7, 1, 6, 2, 5, 8, 0 });

    public void testIsSolvable() {
        assertTrue(b1.isSolvable());
        assertTrue(!b2.isSolvable());
        assertTrue(b3.isSolvable());
        assertTrue(b4.isSolvable());
        assertTrue(!b7.isSolvable());
    }

    public void testManhattan() {
        assertEquals(0, b1.manhattan());
        assertEquals(2, b2.manhattan());
        assertEquals(2, b3.manhattan());
        assertEquals(16, b4.manhattan());
    }

    public void testIsGoal() {
        assertTrue(puzzle.isGoal(b1));
        assertTrue(!puzzle.isGoal(b2));
        assertTrue(!puzzle.isGoal(b3));
        assertTrue(!puzzle.isGoal(b4));
    }

    public void testEquals() {
        assertEquals(b1, b1);
        assertEquals(b4, b5);
        assertFalse(b2.equals(b3));
        assertFalse(b1.equals(null));
        assertFalse(b1.equals(puzzle));
    }

    public void testActionsFor() {
        assertEquals(puzzle.actionsFor(b1), Arrays.asList(Action.RIGHT, Action.DOWN));
        assertEquals(puzzle.actionsFor(b2), Arrays.asList(Action.RIGHT, Action.DOWN));
        assertEquals(puzzle.actionsFor(b3), Arrays.asList(Action.LEFT, Action.RIGHT, Action.DOWN));
        assertEquals(puzzle.actionsFor(b5), Arrays.asList(Action.UP, Action.RIGHT));
    }

    public void testGo() {
        assertEquals(puzzle.go(b5, Action.UP), b6);
    }
}

Depth First Agent

DepthFirstAgent.java
import java.util.LinkedList;
import java.util.List;

/**
 * A problem solving engine for depth-first iterative deepening searching.
 */
public class DepthFirstAgent<S, A> implements Agent<S, A> {
    private Listener listener;

    public DepthFirstAgent(Listener listener) {
        this.listener = listener;
    }

    /**
     * Solves the problem using depth-first iterative deeping.
     */
    public List<A> solve(Problem<S, A> problem, S start) {
        if (listener != null)
            listener.started();

        // Fire off the iterations at increasing limits
        for (int limit = 1; true; limit++) {
            if (listener != null)
                listener.iterationStarted(limit);

            boolean reachedLimit = false;
            LinkedList<Node<S, A>> fringe = new LinkedList<Node<S, A>>();
            fringe.addFirst(new Node<S, A>(start));

            while (!fringe.isEmpty()) {
                Node<S, A> n = fringe.removeFirst();
                if (problem.isGoal(n.getState())) {
                    if (listener != null)
                        listener.solutionFound();
                    return n.actionList();
                }
                if (n.getDepth() > limit) {
                    reachedLimit = true;
                    continue;
                }
                for (A a : problem.actionsFor(n.getState())) {
                    S s = problem.go(n.getState(), a);
                    if (listener != null)
                        listener.generated();
                    fringe.addFirst(new Node<S, A>(s, a, n));
                }
            }

            // Finished an interation. We might need to go to the next
            // iteration, or we might have exhausted the search space.
            if (!reachedLimit) {
                if (listener != null)
                    listener.failed();
                return null;
            }
        }
    }
}

Eight Puzzle Demo

EightPuzzleDemo.java
import java.util.List;

/**
 * An application that runs various solvers on the eight puzzle.
 */
public class EightPuzzleDemo {

    /**
     * Displays the moves in the solution of a random eight puzzle instance.
     */
    public static void main(String[] args) {
        EightPuzzle puzzle = new EightPuzzle();

        EightPuzzle.Board board = new EightPuzzle.Board();
        board = new EightPuzzle.Board();
        System.out.println("Starting board:\n" + board);

        Heuristic<EightPuzzle.Board> manhattanHeuristic = new Heuristic<EightPuzzle.Board>() {
            public int eval(EightPuzzle.Board board) {
                return board.manhattan();
            }
        };

        Listener listener = new Listener();

        List<EightPuzzle.Action> moves =

                // IDA* always works with the manhattan heuristic
                new IdastarAgent<EightPuzzle.Board, EightPuzzle.Action>(manhattanHeuristic, listener).solve(puzzle,
                        board);

        // A* often runs out of memory with manhattan heuristic
        // new AstarAgent<EightPuzzle.Board, EightPuzzle.Action>(
        // manhattanHeuristic, listener).solve(puzzle, board);

        // Plain DFID can take forever
        // new DepthFirstAgent<EightPuzzle.Board, EightPuzzle.Action>(
        // listener).solve(puzzle, board);

        for (EightPuzzle.Action move : moves) {
            board = puzzle.go(board, move);
            System.out.println(move + "\n" + board);
        }

        listener.report();
    }
}

Eight Puzzle Notes

Performance notes on a little example.  Starting with

  1 2 3
  4 5 .
  6 7 8

DFID found a 15-move solution LLURRDLULDRRULL by generating
11,066,435 nodes in 6531 ms.

I have separate implementations of other algorithms, not shown here.
But this is interesting:

IDA* found the same 15-move solution by generating 122,207 nodes
in 234 ms.

A* found the same 15-move solution by generating 69,737 nodes
in 1891 ms.

+---------------------------------------------------------------+
| IDA* generates 75% more nodes as A*, but runs 8 times faster. |
+---------------------------------------------------------------+