Florida Southern Seal
CSC 3280: Data Structures
(Spring 2025)

Syllabus

LMS

Teachers


Assignments


Other Pages

Project 6:
Ready... set... Set!


Assigned: Mon Mar 24 2025
Due: 11:59:00 PM on Sun Mar 30 2025
Team Size: 1
Language: Java
Out of: 100 points


In this project, you will implement a mathematical Set and then write a player for Antonim. Your version of a Set will be called PureSet. Since we're being mathy, your Sets will be immutable! Awesome!

Part 0, 0 points: What should we use to keep track of the elements in a Set? In my implementation, I used an ArrayList<E>, which I named elements. Whatever you choose, be certain to make the accessibility private. If you make it public or protected, then subclasses can modify it, which we need to avoid for immutability. Afterwards, let's declare the field to be final so that it can only be assigned once (presumably in the constructor). My field declaration looks like this:

private final ArrayList<E> elements;

Part 1, 0 points: Since our class will be immutable, we won't be adding or removing elements from it. Thus, we need to start it off with all the elements it will have. We need to write a constructor that takes one element: an ArrayList<E>. First, write the constructor signature. Next, write the Javadoc.

Part 2, 0 points: You may be tempted to write the body of the constructor in one line:

public PureSet(ArrayList<E> elements) {
    this.elements = elements;
}
Unfortunately, this leaves the contents vulnerable to change by code like the following block.
ArrayList<Llama> llamas = new ArrayList<Llama>();
llamas.add(new Llama("Charlie"));
llamas.add(new Alpaca("Bruno"));
PureSet<Llama> llamaSet = new PureSet<Llama>(llamas);
System.out.println(llamaSet); //prints: {Charlie, Bruno}
llamas.add(new Llama("Cuzco"));
System.out.println(llamaSet); //prints: {Charlie, Bruno, Cuzco}
Instead, we need to copy the contents over inside the constructor so that modifying the old list doesn't affect our set.
this.elements = new ArrayList<E>();
for (int i = 0; i < elements.size(); i++) {
    E element = elements.get(i);
    if (!this.elements.contains(element)) {
        this.elements.add(element);
    }
}
Which methods of the parameter are we using that are specific to the ArrayList class? None! Both get() and size() are available in the abstract superclass List. By changing our parameter type in the constructor, we can make our class more flexible for outside code to use! (This is called generalization.) Change the constructor signature to take a List<E> instead of an ArrayList

Part 3, 10 points: Write a toString method. I highly recommend using the squigly-braces like math sets do, because that will make me happy! You can test your code with this:

ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(5);
numbers.add(27);
numbers.add(20013);
PureSet<Integer> set = new PureSet<>(numbers);
assert set.toString().contains("5");
assert set.toString().contains("27");
assert set.toString().contains("20013");
assert !set.toString().contains("1999");
String s = set.toString();
assert s.indexOf("5") == s.lastIndexOf("5");

numbers.add(1999);
set = new PureSet<>(numbers);
assert set.toString().contains("1999");

Part 4, 0 points: In an immutable class, it's important that both:

  • None of the class's methods change the fields and
  • Other programmers can't override the methods given here.
In order to enforce this second issue, we need to make each method final. Change the method signature to
public final String toString()

Part 5, 0 points: Can we generalize our constructor even further? We're using a very common loop traversal pattern:

for (int i = 0; i < X.size(); i++) {
    E object = X.get(i);
    ...
}
This is exactly the sort of situation where we want to use a for-each loop instead. Let's do that! Change the header of the for loop like follows (and remove the first line of the body).
for (E element : elements) {
You can use a for-each loop anytime you want to traverse something that implements the Iterable interface. All Java lists (and more) implement this interface, so we can make this change without adding requirements on the parameter. The body of our constructor is more general now; if we want to make the entire constructor more general, we need to generalize the parameter. Change the constructor header to take an Iterable<E> instead of a List.

Part 6, 10 points: This is going to be an immutable class, so none of the methods should alter the class itself. Instead, each one will be fruitful (a pure function). Let's start by writing a method to test whether an object is an element of a set. Write contains, which takes an element of the generic type and returns whether that element is in the set. Don't forget to make it final after you're done implementing it. Test it thoroughly! You can use code like this for part of your tests:

ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(5);
numbers.add(27);
numbers.add(20013);
PureSet<Integer> set = new PureSet<>(numbers);
assert set.contains(5);
assert set.contains(27);
assert set.contains(20013);
assert !set.contains(1999);

numbers.add(1999);
set = new PureSet<>(numbers);
assert set.contains(1999);

List<String> strings = new ArrayList<>();
PureSet<String> set2 = new PureSet<>(strings);
assert !set2.contains("");

Part 7, 0 points: Notice that your constructor uses the contains method. It's often a good idea to have methods in our class call each other. Let's rewrite the constructor to use our contains instead:

...
if (!this.contains(element)) {
    ...
...

Part 8, 10 points: Implement isSubsetOf, which will take another set and return a boolean indicating whether every element in this set is also in the parameter. Here's a subset of some good tests:

List<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(5);
numbers.add(27);
numbers.add(20013);
PureSet<Integer> set = new PureSet<>(numbers);
assert set.isSubsetOf(set);

List<Integer> numbers2 = new ArrayList<>();
numbers2.add(27);
numbers2.add(5);
PureSet<Integer> set2 = new PureSet<>(numbers2);
assert set2.isSubsetOf(set);
assert !set.isSubsetOf(set2);

numbers2.add(812);
set2 = new PureSet<>(numbers2);
assert !set2.isSubsetOf(set);
assert !set.isSubsetOf(set2);

numbers2.add(20013);
set2 = new PureSet<>(numbers2);
assert !set2.isSubsetOf(set);
assert set.isSubsetOf(set2);

numbers.add(812);
set = new PureSet<>(numbers);
assert set2.isSubsetOf(set);
assert set.isSubsetOf(set2);

numbers.add(812);
set = new PureSet<>(numbers);
assert set2.isSubsetOf(set);
assert set.isSubsetOf(set2);

numbers.add(1024);
set = new PureSet<>(numbers);
assert set2.isSubsetOf(set);
assert !set.isSubsetOf(set2);

Part 9, 10 points: Now implement equals. Hint: There's a really easy way to write this one! Here's some testing code:

List<Integer> numbers = new ArrayList<>();
PureSet<Integer> set = new PureSet<>(numbers);
assert set.equals(set);

PureSet<Integer> set2 = new PureSet<>(numbers);
assert set.equals(set2);

numbers.add(5);
numbers.add(27);
numbers.add(20013);
set = new PureSet<>(numbers);
assert set.equals(set);

List<Integer> numbers2 = new ArrayList<>();
numbers2.add(27);
numbers2.add(5);
set2 = new PureSet<>(numbers2);
assert !set.equals(set2);
assert !set2.equals(set);

numbers2.add(812);
set2 = new PureSet<>(numbers2);
assert !set.equals(set2);
assert !set2.equals(set);

numbers2.add(20013);
set2 = new PureSet<>(numbers2);
assert !set.equals(set2);
assert !set2.equals(set);

numbers.add(812);
set = new PureSet<>(numbers);
assert set.equals(set2);
assert set2.equals(set);

numbers.add(812);
set = new PureSet<>(numbers);
assert set.equals(set2);
assert set2.equals(set);

numbers.add(1024);
set = new PureSet<>(numbers);
assert !set.equals(set2);
assert !set2.equals(set);

Part 10, 10 points: Implement unionWith. The union of two sets is the set that contains all of the elements that occur in either of the two sets. Thus, the union of {1, 2, 3} and {3, 4, 5} is {1, 2, 3, 4, 5}. You can test with this:

List<Integer> numbers = new ArrayList<>();
PureSet<Integer> set = new PureSet<>(numbers);
assert set.unionWith(set).equals(set);

PureSet<Integer> set2 = new PureSet<>(numbers);
assert set.unionWith(set).equals(set2);
assert set.unionWith(set2).equals(set2);
PureSet<Integer> set3 = new PureSet<>(numbers);
assert set.unionWith(set2).equals(set3);

numbers.add(5);
numbers.add(27);
numbers.add(20013);
set = new PureSet<>(numbers);
assert set.unionWith(set).equals(set);
assert set.unionWith(set2).equals(set);
assert set2.unionWith(set).equals(set);
assert !set2.unionWith(set).equals(set2);

List<Integer> numbers2 = new ArrayList<>();
numbers2.add(27);
numbers2.add(5);
set2 = new PureSet<>(numbers2);
assert set.unionWith(set2).equals(set);
assert set2.unionWith(set).equals(set);
assert !set2.unionWith(set).equals(set2);
assert !set.unionWith(set2).equals(set2);

numbers2 = new ArrayList<>();
numbers2.add(2);
numbers2.add(4);
set2 = new PureSet<>(numbers2);
List<Integer> numbers3 = new ArrayList<>();
numbers3.add(2);
numbers3.add(4);
numbers3.add(5);
numbers3.add(27);
numbers3.add(20013);
set3 = new PureSet<>(numbers3);
assert set.unionWith(set2).equals(set3);
assert set2.unionWith(set).equals(set3);
assert !set2.unionWith(set3).equals(set);
assert !set.unionWith(set3).equals(set2);

numbers2.add(20013);
set2 = new PureSet<>(numbers2);
assert set.unionWith(set2).equals(set3);
assert set2.unionWith(set).equals(set3);
assert !set2.unionWith(set3).equals(set);
assert !set.unionWith(set3).equals(set2);

numbers3.add(6);
set3 = new PureSet<>(numbers3);
assert !set.unionWith(set2).equals(set3);
assert !set2.unionWith(set).equals(set3);

Part 11, 10 points: Implement intersectWith. The intersection of two sets is the set that contains all of the elements that occur in both of the two sets. Thus, the intersection of {1, 2, 3, 4} and {3, 4, 5} is {3, 4}. These tests are okay, but not perfect:

List<Integer> numbers = new ArrayList<>();
PureSet<Integer> set = new PureSet<>(numbers);
assert set.intersectWith(set).equals(set);

PureSet<Integer> set2 = new PureSet<>(numbers);
assert set.intersectWith(set).equals(set2);
assert set.intersectWith(set2).equals(set2);
PureSet<Integer> set3 = new PureSet<>(numbers);
assert set.intersectWith(set2).equals(set3);

numbers.add(5);
numbers.add(27);
numbers.add(20013);
set = new PureSet<>(numbers);
assert set.intersectWith(set).equals(set);
assert set.intersectWith(set2).equals(set2);
assert set2.intersectWith(set).equals(set2);
assert !set2.intersectWith(set).equals(set);

List<Integer> numbers2 = new ArrayList<>();
numbers2.add(27);
numbers2.add(5);
set2 = new PureSet<>(numbers2);
assert set.intersectWith(set2).equals(set2);
assert set2.intersectWith(set).equals(set2);
assert !set2.intersectWith(set).equals(set);
assert !set.intersectWith(set2).equals(set);

numbers2 = new ArrayList<>();
numbers2.add(2);
numbers2.add(4);
set2 = new PureSet<>(numbers2);
List<Integer> numbers3 = new ArrayList<>();
numbers3.add(2);
numbers3.add(4);
numbers3.add(5);
numbers3.add(27);
numbers3.add(20013);
set3 = new PureSet<>(numbers3);
PureSet<Integer> empty = new PureSet<Integer>(new ArrayList<Integer>());
assert set3.intersectWith(set2).equals(set2);
assert set2.intersectWith(set3).equals(set2);
assert set3.intersectWith(set).equals(set);
assert set.intersectWith(set3).equals(set);
assert !set2.intersectWith(set3).equals(set);
assert !set.intersectWith(set3).equals(set2);
assert set.intersectWith(set2).equals(empty);

Part 12, 10 points: Implement differenceWith. someSet.differenceWith(otherSet) should return the set of all elements in someSet that are not in otherSet. Here is some testing code:

List<Integer> numbers = new ArrayList<>();
PureSet<Integer> set = new PureSet<>(numbers);
assert set.differenceWith(set).equals(set);

PureSet<Integer> set2 = new PureSet<>(numbers);
assert set.differenceWith(set).equals(set2);
assert set.differenceWith(set2).equals(set2);
PureSet<Integer> set3 = new PureSet<>(numbers);
assert set.differenceWith(set2).equals(set3);

PureSet<Integer> empty = new PureSet<Integer>(new ArrayList<Integer>());
numbers.add(5);
numbers.add(27);
numbers.add(20013);
set = new PureSet<>(numbers);
assert set.differenceWith(set).equals(empty);
assert set.differenceWith(set2).equals(set);
assert set2.differenceWith(set).equals(empty);
assert !set2.differenceWith(set).equals(set);

List<Integer> numbers2 = new ArrayList<>();
numbers2.add(27);
numbers2.add(5);
set2 = new PureSet<>(numbers2);
assert !set.differenceWith(set2).equals(empty);
assert set2.differenceWith(set).equals(empty);
assert !set2.differenceWith(set).equals(set);
assert !set.differenceWith(set2).equals(set);

numbers2 = new ArrayList<>();
numbers2.add(2);
numbers2.add(4);
set2 = new PureSet<>(numbers2);
List<Integer> numbers3 = new ArrayList<>();
numbers3.add(2);
numbers3.add(4);
numbers3.add(5);
numbers3.add(27);
numbers3.add(20013);
set3 = new PureSet<>(numbers3);
assert set3.differenceWith(set2).equals(set);
assert set2.differenceWith(set3).equals(empty);
assert set3.differenceWith(set).equals(set2);
assert set.differenceWith(set3).equals(empty);
assert !set2.differenceWith(set3).equals(set);
assert !set.differenceWith(set3).equals(set2);
assert set.differenceWith(set2).equals(empty);

Part 13, 5 points: It's really helpful to get a list of the elements. Write a method, toList, that takes no parameters and returns a List of the elements. The declared type must not be a specific type of List. It must have this signature:

public List toList()
It's also important that it makes a copy of the list instead of returning the contents itself. (Why is this?) Make sure your implementation returns a copy and not a reference to the same list! Here's some tests:
List<Integer> numbers = new ArrayList<>();
List<Integer> result;
PureSet<Integer> set = new PureSet<>(numbers);
assert set.toList().size() == 0;

numbers.add(5);
numbers.add(27);
numbers.add(20013);
set = new PureSet<>(numbers);
result = set.toList();
assert result.size() == 3;
assert result.contains(5);
assert result.contains(27);
assert result.contains(20013);

numbers.add(32);
assert result.size() == 3;
assert result.contains(5);
assert result.contains(27);
assert result.contains(20013);

set = new PureSet<>(numbers);
result = set.toList();
assert result.size() == 4;
assert result.contains(5);
assert result.contains(27);
assert result.contains(20013);
assert result.contains(32);

Part 14, 0 points: Let's test your code out during actual game play. To compile and run the code locally, you'll need these:

Part 15, 0 points: The game for this project is Antonim, available here: Antonim

Part 16, 15 points: Create your own Antonim player, AntonimPlayer.java. Start off by getting getMove to return a legal move. Remember:

  • Your player should only directly invoke the PureSet methods assigned here. I'll be testing your player with my own copy of PureSet.java, so if you call other methods, your player won't compile and you won't earn any points for it.
  • Don't use randomness in your player. (Randomness is a really powerful tool. If you're interested in writing a player that uses randomness, we should definitely talk after this course is finished!)
  • Don't call the getOptions method.
  • Make sure your player decides its move in a reasonable amount of time. (If it runs too long, I'll have to kill the process. Exponential-time players are probably not going to earn points.)
  • Add a toString method if you want to give your player an interesting name. (Highly recommended.)
You can try it against a random player with code like this:
int numPiles = 5;
int pileSize = 8;
PositionFactory<Antonim> factory = new Antonim.PositionBuilder(numPiles, pileSize);
Player<Antonim> me = new AntonimPlayer();
Player<Antonim> random = new RandomPlayer<Antonim>();
Referee<Antonim> ref = new Referee<>(me, random, factory);
ref.call();

ref.gauntlet(10000);

Part 17, 10 points: Modify your strategy so that your player does well against my random player. You will earn points depending on how often you win in my tests. Warning: I won't tell you the parameters of those tests. I recommend chatting with other teams about the factory parameters they're using and going with the hardest ones. Important: do not copy code from any outside sources, but:

  • It's okay to search online for strategies and info about this game, as long as you're not getting code (in any programming language) or pseudocode.
  • It's okay to talk to other people about strategies so long as you're not sharing code.
  • It's okay to challenge me to a game if that would help!
Here's how many points you earn based on how your player does when I test it:
  • Win ≥45%: 5 points
  • Win ≥74%: 10 points
  • Win ≥91%: 15 points
  • Win ≥99%: 20 points

Submitting your Project:

The files I'm looking for in this project are:

  • PureSet.java
  • AntonimPlayer.java
Please make sure to remove scaffolding from your code so that it doesn't print a ton when executed. Your team name is your username if you're working alone or your members' last names with "And" between them if you're working with a teammate. (For example, alone my teamname would be "kburke", but when working with Stacey Stock it would be "burkeAndStock".) Put completed working files you have in a folder named <YourTeamName>Project6, then zip the folder and upload the resulting .zip file to the assignment's canvas page so I can grade it. (E.g., for me: kburkeProject6.zip or burkeAndStockProject6.zip.) If you do it before the deadline and want me to run it through my tester to see how it does against the random players, send me a message and I'll do it. If you upload it after the deadline, please send me a message so I can grade it ASAP. Please please please name the folder correctly, then zip it; don't rename the zip file, or you'll have to resubmit and may incur a lateness penalty.