3
\$\begingroup\$

This is a programming challenge I set for myself a while back to create an AI that starts with no knowledge of anything whatsoever, and learns as you talk to it. (It can learn stuff like your name, how to say hello, goodbye, etc.)

I tried to use the least amount of hard-coded responses as possible. I'm happy that it works, but I feel like the code behind it is very clumsy and... just not very good. The program is called Genesis. Tell me what you think!

Genesis.java

package genesis;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class Genesis {

public static final String name = "Genesis";
public static final String version = "1.0";

public static void main(String[] args) {
    try {
        new Genesis();
    } catch(IOException e) {
        printError(new PrintWriter(System.err), e);
    }
}

private final FileManager fm;
protected String last = null;

public Genesis() throws IOException {
    log("Initializing " + toString() + "...");
    log("Generating files...");
    fm = new FileManager(this);
    log(toString() + " started on " + System.getProperty("os.name"));
    start();
    stop();
}

public void stop() {
    stop(0);
}

public void stop(int error) {
    if(error == 0)
        log(toString() + " shut down successfully!");
    else
        log(toString() + " shut down with error code: " + error);
    if(fm != null)
        fm.close();
    System.exit(error);
}

public void start() {
    try(BufferedReader r = new BufferedReader(new InputStreamReader(System.in))) {
        System.out.print("You: ");
        String s = r.readLine();
        if(respond(s))
            start();
    } catch(Throwable t) {
        printError(t);
    }
}

public boolean respond(String s) { //decide how and what to respond, return if we should keep the program alive
    if(s.trim().equals(""))
        return true; //nothing to do, but keep the program alive
    String response = "";
    if(last == null) { //first message must always be a greeting
        boolean newg = true;
        for(String r : fm.getResponses(ResponseType.GREETING)) {
            if(transform(s).equalsIgnoreCase(transform(r)))
                newg = false; //if this is a greeting that we know about, we dont need to store it again
        }
        if(newg) //store a new greeting for use in another session (or this one)
            fm.setResponse(ResponseType.GREETING, removeEndPunc(s));
        //give a greeting back!
        System.out.println(response = (name + ": " + format(fm.getResponses(ResponseType.GREETING).get((int) (System.nanoTime() % fm.getResponses(ResponseType.GREETING).size())))));
    } else {
        boolean notg = true;
        for(String r : fm.getResponses(ResponseType.GREETING)) { //check if THE LAST MESSAGE is another greeting
            if(transform(last).equalsIgnoreCase(transform(r)))
                notg = false;
        }
        boolean notf = true;
        for(String r : fm.getResponses(ResponseType.FAREWELL)) { //check if they're saying a known farewell
            if(transform(s).equalsIgnoreCase(transform(r)))
                notf = false;
        }
        if((!notf || s.equalsIgnoreCase("exit")) && notg) { //if they're doing a farewell or saying "exit", and THE LAST MESSAGE is not a greeting
            boolean newf = true;
            for(String r : fm.getResponses(ResponseType.FAREWELL)) { //check if it's a new farewell
                if(transform(last).equalsIgnoreCase(transform(r)))
                    newf = false;
            }
            if(newf) //if it's new, store it for another session (or this one)
                fm.setResponse(ResponseType.FAREWELL, removeEndPunc(last));
            //say bye back
            System.out.println(response = (name + ": " + format(fm.getResponses(ResponseType.FAREWELL).get((int) (System.nanoTime() % fm.getResponses(ResponseType.FAREWELL).size())))));
            return false; //exit the loop
        }
    }

    boolean containsLaugh = false;
    for(String r : fm.getResponses(ResponseType.LAUGH)) { //are they laughing?
        if(s.matches(".*?\\b" + r + "\\b.*?"))
            containsLaugh = true;
    }
    boolean laughIfPossible = false;
    int laugh = 0;
    for(char c : s.toCharArray()) { //very bad laugh detection: >50% h's or l's
        if(c == 'h' || c == 'l')
            laugh++;
    }
    if(laugh > s.toCharArray().length / 2) { //if >50% h's or l's
        boolean newl = true;
        for(String r : fm.getResponses(ResponseType.LAUGH)) { //is this a laugh we know?
            if(transform(s).equalsIgnoreCase(transform(r)))
                newl = false;
        }
        if(newl) //if it's new, save it for later
            fm.setResponse(ResponseType.LAUGH, removeEndPunc(s));
        laughIfPossible = true; //if there's nothing else to say later, laugh
    }

    if(!containsLaugh) { //if super serious business mode is on
        String[] set = s.split("(\\s+is\\s+|\\'s\\s+)"); //regex: split for every "is" or 's ('s as in contraction for is)
        try { //if it's math, solve it
            System.out.println(response = (name + ": " + solve(transform(set[1]).trim())));
        } catch(Throwable t) { //it's not math
            String ek = transform(set[0]).toLowerCase(); //get the first part of the phrase
            if(ek.contains("what")) { //user is asking for information
                String k = transform(reversePerson(join(set, "is", 1).toLowerCase())); //get the object to look up
                for(String values : fm.getResponses(ResponseType.VALUE)) {
                    if(transform(values.split("§=§")[0]).trim().equalsIgnoreCase(k)) //if we know the information, tell the user
                        response = name + ": " + cap(k) + " is " + values.split("§=§")[1].trim() + punc();
                }
                if(!response.equals("")) //only respond if we have something useful to say
                    System.out.println(response);
            } else if(s.contains(" is ")) { //the user is telling us information
                String k = reversePerson(s.split(" is ")[0].toLowerCase().trim()); //the key to store
                String v = join(s.split(" is "), "is", 1).toLowerCase().trim(); //the value to store for the key
                fm.setResponse(ResponseType.VALUE, k + "§=§" + reversePerson(removeEndPunc(v))); //store the key and value
                System.out.println(response = (name + ": " + cap(k) + " is " + removeEndPunc(v) + punc())); //tell the user about our new information
            }
        }
    }
    if(response.trim().equals("") && (laughIfPossible || containsLaugh)) //if we have nothing else to say, but we can laugh, laugh
        System.out.println(response = (name + ": " + cap(fm.getResponses(ResponseType.LAUGH).get((int) (System.nanoTime() % fm.getResponses(ResponseType.LAUGH).size()))))); //say a random laugh
    fm.log("You: " + s); //log what the user said
    fm.log(name + ": " + (response.replace(name + ": ", ""))); //log our response, make sure to include the name even if we didn't earlier
    last = s; //set the new last message
    return true; //keep the program alive
}

private static String join(String[] set, String medium, int offset) { //join an array together with a specified string in between starting at a specified offset
    String s = set[offset];
    int i = 0;
    for(String part : set) {
        if(i > offset)
            s = s + " " + medium + " " + part;
        i++;
    }
    return s;
}

private static String reversePerson(String s) { //reverse between 1st and 3rd person, so Genesis makes sense
    return s.replaceAll("\\byour\\b", "§§m§§y§§").replaceAll("\\byou\\b", "§§m§§e§§").replaceAll("\\bme\\b", "you").replaceAll("\\bmy\\b", "your").replaceAll("\\byours\\b", "§§mi§§ne§§").replaceAll("\\bmine\\b", "yours").replace("§§", "").trim();
}

public static double solve(String c) { //solve math expressions
    Pattern p = Pattern.compile("(\\d+|\\d+\\.\\d+)\\s*(\\+|\\-|\\*|\\/|\\%|\\|)\\s*(\\d+|\\d+\\.\\d+).*"); //<number> <+-*/%|> <number>
    Matcher m = p.matcher(c);
    if(m.matches()) { //if this is a correct math expression
        Double d1 = Double.parseDouble(m.group(1));
        Double d2 = Double.parseDouble(m.group(3));
        while(c.contains("+") || c.contains("-") || c.contains("*") || c.contains("/") || c.contains("%") || c.contains("|")) { //checking for valid operations
            c = c.replaceAll("(\\d)\\.0(\\D)", "$1$2"); //replace all x.0 with just x (it looks better)
            m = p.matcher(c);
            if(!m.matches()) //this SHOULD match
                throw new ArithmeticException();
            switch(m.group(2)) { //check the operation symbol and do math
                default:
                    break;
                case "+":
                    c = c.replaceAll("[" + d1 + "]\\s*\\+\\s*[" + d2 + "]", (d1 + d2) + "");
                    break;
                case "-":
                    c = c.replaceAll("[" + d1 + "]\\s*\\-\\s*[" + d2 + "]", (d1 - d2) + "");
                    break;
                case "*":
                    c = c.replaceAll("[" + d1 + "]\\s*\\*\\s*[" + d2 + "]", (d1 * d2) + "");
                    break;
                case "/":
                    c = c.replaceAll("[" + d1 + "]\\s*\\/\\s*[" + d2 + "]", (d1 / d2) + "");
                    break;
                case "%":
                    c = c.replaceAll("[" + d1 + "]\\s*%\\s*[" + d2 + "]", (d1 % d2) + "");
                    break;
                case "|":
                    c = c.replaceAll("[" + d1 + "]\\s*\\|\\s*[" + d2 + "]", (Integer.parseInt((d1 + "").replace(".0", "")) | Integer.parseInt((d2 + "").replace(".0", ""))) + "");
                    break;
            }
        }
    } //else, maybe it's just a number (return the number)... or maybe it's not even math - if it is, return the parsed answer
    return Double.parseDouble(c);
}

private static String transform(String s) { //transform a string into something the program can read
    return s.toLowerCase().replace("?", "").replace(".", "").replace("!", "").replace(",", "").replace("_", "").replace("~", "").replace("`", "").replace("'", "").replace("\"", "").replace("\"", "").replace("\\", "").replace(":", "").replace(";", "").replace("the", " ").replace("teh", " ").replace("how do", "how can").replace("re", "").replace(" a ", " ").replace("is", "").replace("has", "").replace("get to", "go to").replaceAll("\\Bs\\b", "").replaceAll(" {2}?", "").trim();
}

private static String removeEndPunc(String s) {
    return s.replaceAll("[!\\.\\?]+$", ""); //remove all !'s .'s and ?'s from the end of a string
}

private static String format(String s) { //add random punctuation, and capitalize the first character
    return cap(s) + punc();
}

private static String cap(String s) { //capitalize the first letter of a given string
    String r = s.toUpperCase();
    if(r.length() > 1)
        r = s.replaceFirst(s.substring(0, 1), s.substring(0, 1).toUpperCase());
    return r;
}

private static char punc() { //return random punctuation
    switch((int) System.nanoTime() % 5) {
        default:
        case 0:
        case 1:
        case 2:
        case 3:
            return '.';
        case 4:
            return '!';
    }
}

public FileManager getFileManager() {
    return fm;
}

public void printError(Throwable t) {
    printError(System.err, t);
    if(fm != null)
        printError(fm.getLogStream(), t);
    stop(1);
}

private static void printError(Object output, Throwable t) {
    PrintWriterStream out;
    if(output instanceof PrintWriter)
        out = new PrintWriterStream((PrintWriter) output);
    else if(output instanceof PrintStream)
        out = new PrintWriterStream((PrintStream) output);
    else
        throw new IllegalArgumentException("Output must be of type PrintWriter or PrintStream");
    out.println();
    out.println("A fatal error occurred: " + t.toString());
    out.println();
    out.println("-----=[General Stack Trace]=-----");
    for(StackTraceElement s : t.getStackTrace())
        //print the throwable's stack trace
        out.println(s.getClassName() + "." + s.getMethodName() + "() on line " + s.getLineNumber());
    out.println("-----=[" + name + " Stack Trace]=-----");
    out.println();
    out.println("-----=[" + name + " Stack Trace]=-----");
    boolean fault = false;
    for(StackTraceElement s : t.getStackTrace()) { //filter out the stack trace for only Genesis
        if(s.getClassName().startsWith("genesis")) {
            out.println(s.getClassName() + "." + s.getMethodName() + "() on line " + s.getLineNumber());
            fault = true;
        }
    }
    if(!fault) //if it's not our fault, tell the user
        out.println("This doesn't look like a problem relating to " + name + ". Check the above general stack trace.");
    out.println("-----=[Genesis Stack Trace]=-----");
    out.println();
    out.println("-----=[Remote Stack Trace]=-----");
    fault = false;
    for(StackTraceElement s : t.getStackTrace()) { //filter out the stack trace for only outside Genesis
        if(!s.getClassName().startsWith("genesis")) {
            out.println(s.getClassName() + "." + s.getMethodName() + "() on line " + s.getLineNumber());
            fault = true;
        }
    }
    if(!fault) //if it's not their fault, tell the user
        out.println("This doesn't look like a problem with anything outside " + name + ". Check the above " + name + " stack trace.");
    out.println("-----=[Remote Stack Trace]=-----");
    out.println();
}

public void log(String message) {
    log(System.out, message);
}

public void log(PrintStream out, String message) {
    FileManager.log(out, message);
    if(fm != null)
        fm.log(message);
}

public String toString() {
    return name + " v" + version;
}

static class PrintWriterStream { //super hacky way of combining a PrintWriter and a PrintStream

    private PrintWriter w;
    private PrintStream s;

    PrintWriterStream(PrintWriter w) { //support for PrintWriter
        if(w == null)
            throw new NullPointerException();
        this.w = w;
    }

    PrintWriterStream(PrintStream s) { //support for PrintStream
        if(s == null)
            throw new NullPointerException();
        this.s = s;
    }

    void println() {
        if(w == null && s != null)
            s.println();
        else if(s == null && w != null)
            w.println();
        else
            throw new NullPointerException("No valid output");
    }

    void println(String x) {
        if(w == null && s != null)
            s.println(x);
        else if(s == null && w != null)
            w.println(x);
        else
            throw new NullPointerException("No valid output");
    }

    public void flush() {
        if(w == null && s != null)
            s.flush();
        else if(s == null && w != null)
            w.flush();
        else
            throw new NullPointerException("No valid output");
    }
}

}

FileManager.java

package genesis;

import genesis.Genesis.PrintWriterStream;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;

public final class FileManager {

private final Genesis g;
public static final String folderPath = "Genesis/";
private final File folder = new File(folderPath);
private final File log = new File(folderPath + "log.txt");
private final File resp = new File(folderPath + "responses.txt");
private final PrintWriter lout;
private final PrintWriter rout;

public FileManager(Genesis g) throws IOException {
    this.g = g;
    folder.mkdirs(); //make the Genesis folder if it doesn't exist
    checkFiles(); //make the files
    lout = new PrintWriter(new FileWriter(log, true));
    rout = new PrintWriter(new FileWriter(resp, true));
}

private void checkFiles() { //create new log and response files if they don't exist
    try {
        if (!log.exists())
            log.createNewFile();
        if (!resp.exists())
            resp.createNewFile();
    } catch (IOException e) {
        g.printError(e);
    }
}

public Genesis getGenesis() {
    return g;
}

public File getFolder() {
    return folder;
}

public File getLog() {
    return log;
}

public File getResponsesFile() {
    return resp;
}

public HashMap<ResponseType, List<String>> getResponses() { //get a hashmap of all responsetypes and responses for that type in list form
    checkFiles(); //make sure our files exist first
    HashMap<ResponseType, List<String>> res = new HashMap<ResponseType, List<String>>();
    if (resp.length() == 0) //if we don't have anything, don't return anything
        return res;
    try (BufferedReader r = new BufferedReader(new FileReader(resp))) {
        String line;
        while ((line = r.readLine()) != null) {
            for (ResponseType rt : ResponseType.values()) {
                if (line.split("�")[0].equalsIgnoreCase(rt.name())) { //I could use a different character... but it kept changing back when I moved the project between computers
                    String response = "";
                    for (int i = 1; i < line.split("�").length; i++)
                        response = line.split("�")[i].trim();
                    if (res.get(rt) == null) {
                        List<String> list = new ArrayList<String>();
                        list.add(response);
                        res.put(rt, list);
                    } else
                        res.get(rt).add(response);
                }
            }
        }
    } catch (IOException e) {
        g.printError(e);
    }
    return res;
}

public List<String> getResponses(ResponseType rt) { //get all the responses in list form for a certain response type
    checkFiles();
    List<String> res = new ArrayList<String>();
    if (resp.length() == 0)
        return res;
    try (BufferedReader r = new BufferedReader(new FileReader(resp))) {
        String line;
        while ((line = r.readLine()) != null) {
            if (line.split("�")[0].equalsIgnoreCase(rt.name()))
                res.add(line.split("�")[1].trim());
        }
    } catch (Throwable t) {
        g.printError(t);
    }
    return res;
}

public void setResponse(ResponseType type, String response) { //set a response for a certain type
    response = response.trim();
    try (BufferedReader r = new BufferedReader(new FileReader(resp))) {
        String s;
        while ((s = r.readLine()) != null) {
            if (s.equals(type.toString() + "� " + response))
                return;
        }
        rout.println(type.toString() + "� " + response);
        rout.flush();
    } catch (Throwable t) {
        g.printError(t);
    }
}

public void log(String message) {
    log(lout, message);
}

public static void log(Object output, String message) {
    PrintWriterStream out;
    if(output instanceof PrintWriter)
        out = new PrintWriterStream((PrintWriter) output);
    else if(output instanceof PrintStream)
        out = new PrintWriterStream((PrintStream) output);
    else
        throw new IllegalArgumentException("Output must be of type PrintWriter or PrintStream");
    Calendar c = Calendar.getInstance();
    String hour = c.get(Calendar.HOUR) + "";
    String minute = c.get(Calendar.MINUTE) + "";
    String second = c.get(Calendar.SECOND) + "";
    int ampm = c.get(Calendar.AM_PM);

    if(Integer.parseInt(hour) < 10) //keep the hours minutes and seconds 2 digits
        hour = "0" + hour;
    if(Integer.parseInt(minute) < 10)
        minute = "0" + minute;
    if(Integer.parseInt(second) < 10)
        second = "0" + second;

    String timestamp = "[" + hour + ":" + minute + ":" + second + ":" + (ampm == 0 ? "AM" : "PM") + "]";

    out.println(timestamp + ": " + message);
    out.flush();
}

public PrintWriter getLogStream() {
    return lout;
}

public PrintWriter getResponseStream() {
    return rout;
}

public void close() { //close the log and response file streams
    lout.close();
    rout.close();
}

}

ResponseType.java

package genesis;

public enum ResponseType {

    GREETING, FAREWELL, VALUE, LAUGH;

    public String toString() {
        return name().toLowerCase().replaceFirst(name().substring(0, 1).toLowerCase(), name().substring(0, 1).toUpperCase());
    }

    public static ResponseType getResponseType(String name) {
        for (ResponseType r : values()) {
            if(r.name().equalsIgnoreCase(name))
                return r;
        }
        return null;
    }

}

As a side note, you can compile this code yourself and export it as a runnable JAR file. It creates a folder called Genesis wherever the JAR is, which consists of a log.txt file and a responses.txt file. Log file is what you think it is, and responses file is for use by Genesis, so it knows how to respond to you over time.

Rules for talking to it correctly are as follows:

  • First message must be a greeting (hello/hi/etc)
  • Last message must be a farewell (goodbye/bye/etc)
  • After your farewell, type "exit" to exit the program correctly
\$\endgroup\$
2
\$\begingroup\$

Advice 1: Package names

The package name genesis is not quite informative what software it contains. The common notation is to use the reversed name of the web address of your company. For example, you could use net.genesis.ai for the package name.

Advice 2: ResponseType

You could use the valueOf static method for converting a string to an corresponding enumeration:

public static ResponseType getResponseType(String name) {
    try {
        return ResponseType.valueOf(name);
    } catch (IllegalArgumentException ex) {
        return null;
    }
}

What comes to toString, I suggest you prepend @Override to it: if someone makes a typo in the name toString, @Override will force your IDE (i.e., NetBeans) to underline it with red. Also, in the same method, you could save some typing:

@Override
public String toString() {
    char[] chars = name().toLowerCase().toCharArray();
    chars[0] = Character.toUpperCase(chars[0]);
    return new String(chars);
}

Also, an enumeration may have satellite data:

public enum ResponseType {

    GREETING("Greeting"),
    FAREWELL("Farewell"), 
    VALUE("Value"), // <- Note the comma.
    LAUGH("Laugh"); // <- Note the semicolon.

    private final String humanReadableName; // <- That is were each enumeration knows its satellite data.

    ResponseType(String humanReadableName) {
        this.humanReadableName = humanReadableName;
    }

    public static ResponseType getResponseType(String name) {
        try {
            return ResponseType.valueOf(name);
        } catch (IllegalArgumentException ex) {
            // valueOf throws this when no suitable enumeration found.
            return null;
        }
    }
}

Advice 3

public HashMap<ResponseType, List<String>> getResponses() { //get a hashmap of all responsetypes and responses for that type in list form
    ...
    HashMap<ResponseType, List<String>> res = new HashMap<ResponseType, List<String>>();

I suggest you program to interface, not implementation:

Map<ResponseType, List<String>> res = new HashMap<ResponseType, List<String>>();

Advice 4

As an addition to the 3rd advice, since Java 7 you can use so called diamond inference:

Map<ResponseType, List<String>> res = new HashMap<>();

Advice 5

There is inconsistency with placing a white space character between if and opening (: if( vs. if (. The common way is to place a white space character between the two tokens.

\$\endgroup\$
  • \$\begingroup\$ advice three should be Map<ResponseType,... probably what you intended to say. \$\endgroup\$ – Imus Jan 23 at 15:03
1
\$\begingroup\$

Instead of using short variable names, I recommend you use descriptive ones.

It is always better to use a more descriptive variable name than to use a short name which requires a comment to explain what it is.

Also, I don’t suggest using the opposite of negation (!not) as this also can confuse the reader who is trying to make sense of the code.

\$\endgroup\$

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.