Card-Forge / forge

An unofficial rules engine for the world's greatest card game.
https://card-forge.github.io/forge/
GNU General Public License v3.0
972 stars 561 forks source link

Feature: try to use Threading #5692

Open Hanmac opened 2 months ago

Hanmac commented 2 months ago

For Loops that shouldn't alter object, but only filter them should try to be optimized using Threading

In the Future, that might help to check which creatures should attack or block better.

We also need to see if we should do more Threading on Desktop than on Mobile?

tool4ever commented 2 months ago

My favorite use case for this would be spinning off one chooseSpellAbilityToPlay() call to run as simulation This would allow some sort of hybrid approach and still improve AI capabilities (fall back to heuristics if no better gamestate score gets found after X seconds)

For the speedup we need to make sure the threads can then run on different cores if that doesn't work by default then

Hanmac commented 2 months ago

@tool4ever I wanted to use some collection/filter stuff first, because these should have the least amount of side effects

Hanmac commented 2 months ago

@Agetian stream().parallel() might do the work for us, but i need to experiment with that

Jetz72 commented 2 months ago

Could multiple AI begin processing their actions in parallel when priority starts being passed around, acting on the assumption that other players are going to just pass? Then either cancel and restart the computation if a player before them takes an action, or keep to their intent if priority gets to them and nothing has changed about the gamestate?

Hanmac commented 2 months ago

Could multiple AI begin processing their actions in parallel when priority starts being passed around, acting on the assumption that other players are going to just pass? Then either cancel and restart the computation if a player before them takes an action, or keep to their intent if priority gets to them and nothing has changed about the gamestate?

Making "interruptible" Threads is far more into the future

for now, the goal is to optimize smaller parts

Hanmac commented 2 months ago

@Agetian @tehdiplomat @kevlahnota one of my ideas was trying to optimize the condition checks done in the triggers, like TriggerSpellAbilityCastOrCopy

And if we could run them in parallel, and break early when one of them returns false?

There is an example of one of my ideas for this, the sleep should symbolize the processing time. If it runs in parallel, it still executes the third one, even it it doesn't need it anymore :/

http://tpcg.io/_G3N0EX

/* Online Java Compiler and Editor */

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream; 
import java.util.function.Supplier;

public class HelloWorld{

     public static void main(String []args) throws InterruptedException {
        Stream.Builder<Supplier<Boolean>> builder = Stream.builder();
        builder.add(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("first");
            }
            catch(InterruptedException e) {}
            return true;
        });
        builder.add(() -> {
            try {
                TimeUnit.SECONDS.sleep(10);
                System.out.println("second");
            }
            catch(InterruptedException e) {}
            return false;
        });
        builder.add(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
                System.out.println("third");
            }
            catch(InterruptedException e) {}
            return false;
        });

        Stream<Supplier<Boolean>> stream = builder.build();
        //System.out.format("count: %d", stream.parallel().count());
        //System.out.format("allMatch: %s", stream.allMatch(s -> s.get()) ? "true" : "false");
        System.out.format("allMatch: %s", stream.parallel().allMatch(s -> s.get()) ? "true" : "false");
     }
}
tehdiplomat commented 2 months ago

Sounds like a good approach. Forge has lots of bad architectural decisions that would need to be split to be able to utilize improved threading in these areas..

kevlahnota commented 2 months ago

On my local copy, when using threads or any parallel computation that involves shuffling or reversing instance of FCollection, it will crash.. I use this helper class

package forge.util;

import forge.util.collect.FCollection;

import java.util.*;

public class CollectionUtil {
    public static <T> void shuffle(List<T> list) {
        shuffle(list, MyRandom.getRandom());
    }

    public static <T> void shuffle(List<T> list, Random random) {
        if (list instanceof FCollection) {
            //long start = System.nanoTime();
            shuffleList(list, random);
            //long finish = System.nanoTime();
            //System.out.println("Time nano: " + (finish - start));
        } else {
            Collections.shuffle(list, random);
        }
    }

    public static <T> void shuffleList(List<T> a, Random r) {
        int n = a.size();
        for (int i = 0; i < n; i++) {
            int change = i + r.nextInt(n - i);
            swap(a, i, change);
        }
    }

    private static <T> void swap(List<T> a, int i, int change) {
        T helper = a.get(i);
        a.set(i, a.get(change));
        a.set(change, helper);
    }

    public static <T> void reverse(List<T> list) {
        if (list == null || list.isEmpty())
            return;
        if (list instanceof FCollection) {
            //long start = System.nanoTime();
            reverseWithRecursion(list, 0, list.size() - 1);
            //long finish = System.nanoTime();
            //System.out.println("Time nano: " + (finish - start));
        } else {
            Collections.reverse(list);
        }
    }

    public static <T> void reverseWithRecursion(List<T> list) {
        if (list.size() > 1) {
            T value = list.remove(0);
            reverseWithRecursion(list);
            list.add(value);
        }
    }

    public static <T> void reverseWithRecursion(List<T> list, int startIndex, int lastIndex) {
        if (startIndex < lastIndex) {
            T t = list.get(lastIndex);
            list.set(lastIndex, list.get(startIndex));
            list.set(startIndex, t);
            startIndex++;
            lastIndex--;
            reverseWithRecursion(list, startIndex, lastIndex);
        }
    }
}

and replace all Collection,shuffle(..) and Collection.reverse(...) to use CollectionUtil.shuffle(...) and CollectionUtil.reverse(...)

Hanmac commented 2 months ago

@kevlahnota my first try will be using Threads on cases where the Main list isn't altered, like Filtering.