You can use CodeDom to compile C# code into assembly, and then run this assembly inside your code.
If this code would run in same thread as your game you have to make sure that the code that they write will not block your game code. You can use this by using yield, that will generate state machine on behind.
So you could have something like this:
public interface IAction {
boolean IsFinished();
void Update(GameTime gameTime);
}
public class Wait : IAciton {
private TimeSpan waitingTime;
public Wait(TimeSpan waitingTime) { this.waitingTime = waitingTime; }
boolean IsFinished() { return this.waitingTime <= 0; }
void Update(GameTime gameTime) { this.waitingTime - gameTime.ElaspedGameTime; }
}
and then users would have to write code like this:
public MyMonster : Brain {
public IEnumerable<IAction> Think() {
while (IsAlive) {
yield Wait(TimeSpan.FromSeconds(10));
yield Suicide();
}
}
}
Then you would process it like this:
public AISystem {
public void Update() {
foreach (Brain brain in Brains) {
var enumerator = brain.Think().GetEnumerator();
enumerator.Current.Update();
if (enumerator.Current.IsFinished()) {
enumerator.MoveNext();
}
}
}
}
Now even if there is infinite loop, the code will not block your code. And users can write logic "sequentially".
As you can see this is essentially what unity does in its Behaviors
(I wrote this from head so it has probably some errors in it)
Or you can write your own CIL interpreter and interpret few instructions each frame :)