EDIT: Your update to the question clarifies a lot of things, and changes the direction of the question quite a bit (at least from my perspective).
The problem here is that I would like to be able to not even present invalid action in the first place or grey out the corresponding UI elements.
The problem seems to be that not only would you like to achieve this, but you'd like to achieve it in some completely generic way, which allows you to vary the rule engine behind the scenes dynamically. The latter is the really difficult part, and I'm not sure you're going to find a complete solution readily available for your exact needs.
If we take a step back, the "traditional" way of doing this would be something along the lines of the following (let's use a Model-View-Controller architecture, it's probably a good fit for your game):
The model represents the logic of your game, and is responsible for creating a structure which contains all the information that your presentation layer (the view) needs to do its job (preferably without any additional logic in the view). In practical terms, this means it will figure out exactly what actions are available to the player, which items are "sellable", etc.
The view would be in charge of how this information is presented to the user. Typically, the view is either a server-side template (which results in actual HTML being generated), or a client-side template or other javascript code which results some HTML being updated on the client side (in the latter case, the model is typically sent to the browser as AJAX).
The controller's job is simply to take incoming requests and to route the request to the relevant model, and send this result to the relevant view.
So essentially, if you've designed everything cleverly, the basic MVC pattern already gives a nice separation of the presentation from the logic. What you need to really figure out how to structure your model so that the view can remain relatively void of logic, and generic. Some things are easy (e.g. this list of items can be sold by the player) and some things are hard (here's a list of generic actions - if the player chooses action A, they need to select an item worth X to trade, and a player from their friends list). By carefully considering the axes along which you wish to remain flexible, you can come up with the correct architecture, however.
Changing and tweaking the ruleset
Assuming that the different rule-sets still fit into the same presentation strategy, varying the rule-set will be "relatively easy" to do. It's difficult to say more without knowing your exact game ideas, but you could have hooks in your core game rules for various modifications to apply their changes, etc. If you are looking for something more generic than that, consider reading up on rule engines, or even building your own domain specific language, which you can then translate into rules at runtime.
Original bit about MMOs:
Yannis' links go into some detail regarding MMO architectures. It might not be referring to browser-based games, but much of it is still applicable. A few things I would like to point out:
You seem to be alluding to having much of the game run purely on the client-side. Here's the problem with that: the client cannot be trusted. It's an old rule of thumb for any kind of development, and it applies to games too. It is trivial to mess with javascript code, and there are no purely client-side defenses for that. If this is a concern for you (say, your players' enjoyment or money is at stake), you have to do some work on the server side. Here's an example discussion over at gamedev (coincidentally, that site might be better suited to this question), where the advice is "Client-side movement handled immediately and then verified by the server is a great method when combined with server proxies with interpolated moves". Essentially, you want the server to know quite a lot about what's going on in the client, enough to be able to call foul if someone is tampering with the client. This is the approach used by most realtime multiplayer games (Battlefield, etc) - obviously you can't just trust the client to say "hey, I shot that guy, no, really", you need the server to be able to check that it's at least plausible. We're basically talking about mirroring the client logic and data on the server side.
This is obviously not the easiest thing to achieve, and will probably slow down your development. My advice would be to figure out exactly why you'd like this game to be heavily client-side (cost, user experience, etc), and to consider moving most of the functionality into the server-side. If you do this, your game becomes very similar to a tradition web application, where you just validate all user inputs and run the logic on the server-side. Many current browser "MMOs" actually use this approach, the good ones just happen to do it slick enough that you don't notice.
Note: I put MMO in quotes at the end, because if games like Evony are MMOs, then Gmail is one, too.
I've sorted it out and in the end it went pretty well so I'll give here my recommendations according this painful experience.
(these tips apply for WPF)
Validation : use INotifyDataErrorInfo instead of rules in XAML, your validation takes place in the object instead and even it's sort of complicate to setup it is really worth it. This interface is appropriate for validation of a complex object. Here's an excellent article showing it : http://anthymecaillard.wordpress.com/2012/03/26/wpf-4-5-validation-asynchrone/ I'd suggest you to not copy/paste it but write it manually. (I did and it took me less time than copy/paste and trying to grasp what it does later)
Updating your object whenever user inputs values : it's clearly better to do nothing during controls events such as TextChanged but to stick to validation as mentioned above. Hook to Validation.Error events of your controls for managing the logic of your API (in my case when 'name' is set the user cannot search by 'familiarity' so I disabled appropriate controls).
That leds to a search button that gets enabled only when all values are correct and this is actually the place where you'd grab all params and your stuff, all in one place, not scattered anymore through events of controls.
Also, do not hesitate to use a middle-man object even though it won't contain all the fields of the original object it still proved to be helpful as that's where I implemented INotifyDataDataErrorInfo. There was absolutely no way that I modify the source object which is fine and part of a library anyway ! For multiple choices a user can do on a ListBox, I didn't use it, I just populated them and that's it. (they are all valid by nature, in my case)
Now a few screenshots and explanations.
Here's the API, not about 2 or 3 fields but 21, when I saw it first I knew it would be a pain to implement but still I underestimated it ...
Here the search button gets enabled only when validation is correct,
Using an extremely simple method, luckily in my case only these 3 cases were needed.
private void ToggleSearch()
{
var b1 = GetHasErrorPropertyValue(TextBoxArtistLocation);
var b2 = GetHasErrorPropertyValue(TextBoxDescription);
var b3 = GetHasErrorPropertyValue(TextBoxName);
ButtonSearch.IsEnabled = !(b1 || b2 || b3);
}
private static bool GetHasErrorPropertyValue(DependencyObject dependencyObject)
{
if (dependencyObject == null) throw new ArgumentNullException("dependencyObject");
return (bool)dependencyObject.GetValue(Validation.HasErrorProperty);
}
// Hooking to Validation.Error attached property, these boxes all point to here.
private void TextBox_OnError(object sender, ValidationErrorEventArgs e)
{
ToggleSearch();
}
Here's the output of the query, well the only bothering stuff compared to previous version is that for testing I have to search every time to see the result of the built query for which I assigned Alt-S as a time-saver. Previously I was fetching the result on each value changed in each control, while it looked simple as these were 5 lines methods it soon went to a nightmare. With the new method, I have one hand setting the fields with mouse the other pressing Alt-S and obviously a much simpler class to understand/use/debug.
Last screen : as you can see there isn't much in it, the 3 top methods each call one of the methods below, I think they are self-explanatory. The helper methods are used for setting/retrieving values from controls and to form the query.
Last two samples :
Populating data :
private async Task PopulateData()
{
string apiKey = App.GetApiKey();
var years = GetDescribedEnum<ArtistSearchYear>().ToList();
ComboBoxArtistEndYearAfter.ItemsSource = years;
ComboBoxArtistEndYearBefore.ItemsSource = years;
ComboBoxArtistStartYearAfter.ItemsSource = years;
ComboBoxArtistStartYearBefore.ItemsSource = years;
var rankTypes = GetDescribedEnum<ArtistSearchRankType>().ToList();
ComboBoxRankType.ItemsSource = rankTypes;
var results = Enumerable.Range(0, 101).Select(s => s.ToString(CultureInfo.InvariantCulture)).ToList();
results.Insert(0, string.Empty);
ComboBoxResults.ItemsSource = results;
var sort = GetDescribedEnum<ArtistSearchSort>().OrderBy(s => s.Description).ToList();
ComboBoxSort.ItemsSource = sort;
var start = Enumerable.Range(0, 3).Select(s => (s * 15).ToString(CultureInfo.InvariantCulture)).ToList();
start.Insert(0, string.Empty);
ComboBoxStart.ItemsSource = start;
var buckets = GetDescribedEnum<ArtistSearchBucket>().OrderBy(s => s.Description).ToList();
ListBoxBuckets.ItemsSource = buckets;
var genres = await Queries.ArtistListGenres(apiKey);
ListBoxGenre.ItemsSource = genres.Genres.Select(s => s.Name);
var styles = await Queries.ArtistListTerms(apiKey, ArtistListTermsType.Style);
ListBoxStyle.ItemsSource = styles.Terms.Select(s => s.Name);
var moods = await Queries.ArtistListTerms(apiKey, ArtistListTermsType.Mood);
ListBoxMood.ItemsSource = moods.Terms.Select(s => s.Name);
}
Retrieving parameters :
private void RunSearch()
{
var parameters = new ArtistSearchParameters();
var year = (Func<ComboBox, ArtistSearchYear?>)((c) =>
{
if (c.SelectedValue == null) return null;
var value = GetDescribedObjectValue<ArtistSearchYear>(c.SelectedValue);
return value == ArtistSearchYear.Invalid ? (ArtistSearchYear?)null : value;
});
parameters.ArtistEndYearAfter = year(ComboBoxArtistEndYearAfter);
parameters.ArtistEndYearBefore = year(ComboBoxArtistEndYearBefore);
parameters.ArtistStartYearAfter = year(ComboBoxArtistStartYearAfter);
parameters.ArtistStartYearBefore = year(ComboBoxArtistStartYearBefore);
if (ComboBoxRankType.SelectedValue == null)
{
parameters.RankType = null;
}
else
{
var value = GetDescribedObjectValue<ArtistSearchRankType>(ComboBoxRankType.SelectedValue);
parameters.RankType = value == ArtistSearchRankType.Invalid ? (ArtistSearchRankType?)null : value;
}
var selectedValue = ComboBoxSort.SelectedValue;
if (selectedValue == null)
{
parameters.Sort = null;
}
else
{
var value = GetDescribedObjectValue<ArtistSearchSort>(selectedValue);
parameters.Sort = value == ArtistSearchSort.Invalid ? (ArtistSearchSort?)null : value;
}
parameters.Start = StringToNullableInt(GetSelectorValue<string>(ComboBoxStart));
parameters.Results = StringToNullableInt(GetSelectorValue<string>(ComboBoxResults));
parameters.MaxFamiliarity = SliderMaxFamiliarity.Value < 1.0d ? (double?)SliderMaxFamiliarity.Value : null;
parameters.MinFamiliarity = SliderMinFamiliarity.Value > 0.0d ? (double?)SliderMinFamiliarity.Value : null;
parameters.MaxHotttnesss = SliderMaxHottness.Value < 1.0d ? (double?)SliderMaxHottness.Value : null;
parameters.MinHotttnesss = SliderMinHottness.Value > 0.0d ? (double?)SliderMinHottness.Value : null;
if (CheckBoxFuzzyMatch.IsChecked.HasValue)
parameters.FuzzyMatch = CheckBoxFuzzyMatch.IsChecked.Value ? (bool?)true : null;
if (CheckBoxLimit.IsChecked.HasValue)
parameters.Limit = CheckBoxLimit.IsChecked.Value ? (bool?)true : null;
var buckets =
GetSelectedItems<DescribedObject<ArtistSearchBucket>>(ListBoxBuckets)
.OrderBy(s => s.Description)
.Select(s => s.Value)
.ToList();
parameters.Buckets = buckets.Count > 0 ? buckets : null;
var genres = GetSelectedItems<string>(ListBoxGenre).OrderBy(s => s).ToList();
parameters.Genres = genres.Count > 0 ? genres : null;
var moods = GetSelectedItems<string>(ListBoxMood).OrderBy(s => s).ToList();
parameters.Moods = moods.Count > 0 ? moods : null;
var style = GetSelectedItems<string>(ListBoxStyle).OrderBy(s => s).ToList();
parameters.Styles = style.Count > 0 ? style : null;
parameters.ArtistLocation = string.IsNullOrEmpty(TextBoxArtistLocation.Text)
? null
: TextBoxArtistLocation.Text;
parameters.Description = string.IsNullOrEmpty(TextBoxDescription.Text)
? null
: Regex.Split(TextBoxDescription.Text, @", ?")
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToList();
if (string.IsNullOrEmpty(TextBoxName.Text))
{
parameters.Name = null;
}
else
{
parameters.Name = TextBoxName.Text;
parameters.MaxFamiliarity = null;
parameters.MaxHotttnesss = null;
parameters.MinFamiliarity = null;
parameters.MinHotttnesss = null;
}
}
In my case nullables were vital as they allowed me to discard values when building the query.
For building the query I cheated somehow using JSON :
protected static string GetUrlParameters(string json)
{
if (json == null) throw new ArgumentNullException("json");
JsonTextReader reader = new JsonTextReader(new StringReader(json));
string path = string.Empty;
List<string> list = new List<string>();
while (reader.Read())
{
JsonToken type = reader.TokenType;
if (type == JsonToken.PropertyName)
{
path = reader.Path;
}
else
{
bool b0 = type == JsonToken.Integer;
bool b1 = type == JsonToken.Float;
bool b2 = type == JsonToken.String;
bool b3 = type == JsonToken.Boolean;
if (b0 || b1 || b2 || b3)
{
var value = reader.Value.ToString();
if (b3) value = value.ToLower();
string item = string.Format("{0}={1}", path, value);
list.Add(item);
}
}
}
return string.Join("&", list);
}
DescribedObject is a middle-man object that holds the desired value and the description is fetched from a value [DescriptionAttribute]
public class DescribedObject
{
private readonly string _description;
private readonly Object _value;
public DescribedObject() // NOTE : for design-time
{
}
protected DescribedObject(Object value, string description)
{
_value = value;
_description = description;
}
public string Description
{
get { return _description; }
}
public Object Value
{
get { return _value; }
}
public override string ToString()
{
return string.Format("Value: {0}, Description: {1}", _value, _description);
}
}
public class DescribedObject<T> : DescribedObject
{
public DescribedObject(T value, string description)
: base(value, description)
{
}
public new T Value
{
get
{
return (T)base.Value;
}
}
}
A described object :
[JsonConverter(typeof(PropertyEnumConverter))]
public enum ArtistSearchYear
{
[Description(null), JsonProperty(null)]
Invalid = 0,
[Description("1970"), JsonProperty("1970")]
Year1970,
[Description("1971"), JsonProperty("1971")]
Year1971
}
(PropertyEnumConverter is a simple object that will convert these values according their JsonProperty attribute.)
So as you can see, I kept it stupidly simple but it took me some time as at first I had a more complex approach, again the simplicity proven really helpful though it sounds stupid sometimes.
I haven't talked about the XAML side, briefly there's absolutely nothing in it beside controls and templates, only bindings have these two properties set for them to validate. (also, the object implements INotifyPropertyChanged)
<TextBox x:Name="TextBoxArtistLocation"
Style="{StaticResource TextBoxInError}"
Validation.Error="TextBox_OnError"
Text="{Binding ArtistLocation, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
I hope this answer will help someone, I've tried to be as concise as possible but if you have any question/comment, ask below.
Best Answer
For a similar complex project we implemented an interpreter in the business-layerer with formulas for "isValid" and "isVisible" for every form-element
For the interpreter we used UML-s Object Constraint Language which was once designed for that purpose.
Unfortunately nearly nobody speaks "uml-ocl" so finding somebody to maintain the rules is difficuilt.
If we had to do that again we would choose a more common language like js/vb-script for the interpreter