Skip to content

Commit

Permalink
Merge pull request #624 from CelticMinstrel/lua_formula_bridge
Browse files Browse the repository at this point in the history
Many new features in the formula engine
  • Loading branch information
CelticMinstrel committed Mar 18, 2016
2 parents e5b3d81 + 229e888 commit bb510a5
Show file tree
Hide file tree
Showing 26 changed files with 1,493 additions and 358 deletions.
71 changes: 71 additions & 0 deletions changelog
Expand Up @@ -21,6 +21,77 @@ Version 1.13.4+dev:
(where "array" is a table whose keys are all integers). This joins
the elements of the array with commas and produces a single string
value. eg {x = {1,2,3}} is equivalent to {x = "1,2,3"}.
* Wesnoth formula engine:
* Formulas in unit filters can now access nearly all unit attributes
* New syntax features:
* String interpolation syntax. Within a formula string (enclosed in
'single quotes'), the syntax [some_formula] interpolates the result
of the inner formula into the string. (The simplest use case is
interpolating the values of variables.)
* String can now escape special characters:
['] single quote, [(] open square bracket, [)] close square bracket
* New 'in' operator which tests if a list contains an item or if a map
contains a key.
* New concatenation operator a..b which works on strings and lists
* New range operator a~b which produces a list of consecutive integers
This can also be used for "list slicing" - eg my_list[3~5] returns
a new list containing elements 3 through 5 of my_list.
* Lists can be used as an index for a list. This is "selection indexing"
and returns a new list with only the elements specified by the indexing
list.
* Function definitions (using the def keyword) are now supported in all
formula contexts, which means that they can be used outside FormulaAI.
However, non-FormulaAI functions are currently local to the formula
that declares them.
* Maps containing string keyss that are valid identifiers can now be
indexed with the dot operator instead of the indexing operator.
* Strings can now be indexed via 'string'.char[n]. Also supported are
'string'.word[n] and 'string'.item[n] (the latter splits on commas)
* Changes to core functions:
* head() takes an optional argument - if present, a sublist is returned.
* abs(), max(), min() now work on decimal numbers
* reduce() function can specify an optional initial accumulator
This will be returned for an empty list instead of null.
* substring() function can now accept a negative size parameter
This counts backwards from the specified offset
A size of -1 is the same as 1.
* if() can take two arguments; returns null if the condition is false
* debug_print() now shows in console if debug mode is on
* New core functions:
* Trig functions tan, acos, asin, atan have been added. (Sin and cos
existed since at least 1.9 but were undocumented until very recently.)
* Other common math functions - root(), sqrt(), cbrt(), log(), exp()
* hypot(x,y) function calculates sqrt(x*x+y*y) with minimal error
* pi() returning the circle ratio
* tail() - opposite of head()
* reverse() function for strings and lists
* zip() function - converts [[1,2,3],[4,5,6]] to [[1,4],[2,5],[3,6]]
* take_while() function returns items from a list until the first one
that fails a condition
* find_string() locates a substring within a string
* replace() replaces a sequence within a string
* type() function checks the type of a formula result
* distance_between() - this was promoted from FormulaAI to core
* Bugfixes:
* Dice operator is now synced (where possible)
* Modulus (%) operator now works on decimal numbers
* Exponentiation (^) operator is now right-associative
* List/map indexing now has highest precedence of any operator (instead of lowest)
* Fix several math operations returning a very large negative number when
the operation was invalid (for example, (-2) ^ 0.5).
Now they return null instead.
* Formula debugger (accessed with the debug() function):
* Now works again, but is skipped unless in debug mode
* No longer explodes on formulas using less-than
* Some better information and formatting in the execution trace
* You can now step through "where" variable assignments
* Deprecated:
* The fai/faiend keywords are deprecated as part of a move to clearly
define the difference between "Wesnoth Formula Language", the language,
and "FormulaAI", the AI engine that uses the language. For now they
still work, but any future code should use wfl/wflend instead.
Use of the .fai file extension is still fine for FormulaAI code;
for other formula code in a separate file, .wfl is recommended instead.

Version 1.13.4:
* Language and i18n:
Expand Down
2 changes: 1 addition & 1 deletion data/gui/default/widget/toggle_button_orb.cfg
Expand Up @@ -9,7 +9,7 @@
y = 0
w = {SIZE}
h = {SIZE}
name = "('buttons/misc/orb{STATE}.png" + <<~RC(magenta>{icon})')>>
name = "('buttons/misc/orb{STATE}.png~RC(magenta>[icon])')"
[/image]
#enddef

Expand Down
30 changes: 30 additions & 0 deletions data/test/scenarios/filter_this_unit.cfg
Expand Up @@ -58,3 +58,33 @@
)}
[/event]
)}

{GENERIC_UNIT_TEST filter_fai_unit (
[event]
name=prestart
[modify_unit]
[filter]
id=bob
[/filter]
moves=3
[/modify_unit]
{RETURN (
[have_unit]
id=bob
formula="moves < max_moves"
[/have_unit]
)}
[/event]
)}

{GENERIC_UNIT_TEST filter_fai_unit_error (
[event]
name=prestart
{RETURN (
[have_unit]
id=bob
formula="+ max_moves"
[/have_unit]
)}
[/event]
)}
4 changes: 4 additions & 0 deletions projectfiles/Xcode/Wesnoth.xcodeproj/project.pbxproj
Expand Up @@ -158,6 +158,7 @@
91DCA68A1C9066CC0030F8D0 /* unit_preview_pane.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91DCA6871C9066CC0030F8D0 /* unit_preview_pane.cpp */; };
91DCA68D1C9066EC0030F8D0 /* unit_recruit.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91DCA68B1C9066EC0030F8D0 /* unit_recruit.cpp */; };
91DCA68E1C9066EC0030F8D0 /* unit_recruit.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91DCA68B1C9066EC0030F8D0 /* unit_recruit.cpp */; };
91DCA6901C9360610030F8D0 /* test_formula_core.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91DCA68F1C9360610030F8D0 /* test_formula_core.cpp */; };
91ECD5D21BA11A5200B25CF1 /* unit_creator.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91ECD5D01BA11A5200B25CF1 /* unit_creator.cpp */; };
91ECD5D51BA11A6400B25CF1 /* mp_replay_controller.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91ECD5D31BA11A6400B25CF1 /* mp_replay_controller.cpp */; };
91F462841C71139C0050A9C9 /* preferences_dialog.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91F462821C71139B0050A9C9 /* preferences_dialog.cpp */; };
Expand Down Expand Up @@ -1493,6 +1494,7 @@
91DCA6881C9066CC0030F8D0 /* unit_preview_pane.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = unit_preview_pane.hpp; sourceTree = "<group>"; };
91DCA68B1C9066EC0030F8D0 /* unit_recruit.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = unit_recruit.cpp; sourceTree = "<group>"; };
91DCA68C1C9066EC0030F8D0 /* unit_recruit.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = unit_recruit.hpp; sourceTree = "<group>"; };
91DCA68F1C9360610030F8D0 /* test_formula_core.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = test_formula_core.cpp; sourceTree = "<group>"; };
91ECD5D01BA11A5200B25CF1 /* unit_creator.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = unit_creator.cpp; sourceTree = "<group>"; };
91ECD5D11BA11A5200B25CF1 /* unit_creator.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = unit_creator.hpp; sourceTree = "<group>"; };
91ECD5D31BA11A6400B25CF1 /* mp_replay_controller.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = mp_replay_controller.cpp; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3835,6 +3837,7 @@
B597C4A90FACD42E00CE81F5 /* main.cpp */,
B597C4A80FACD42E00CE81F5 /* test_config_cache.cpp */,
B597C4A70FACD42E00CE81F5 /* test_formula_ai.cpp */,
91DCA68F1C9360610030F8D0 /* test_formula_core.cpp */,
B597C4AD0FACD42E00CE81F5 /* test_lexical_cast.cpp */,
B597C4AC0FACD42E00CE81F5 /* test_network_worker.cpp */,
B597C4AA0FACD42E00CE81F5 /* test_serialization.cpp */,
Expand Down Expand Up @@ -5555,6 +5558,7 @@
91DCA6861C9066A60030F8D0 /* unit_preview_pane.cpp in Sources */,
91DCA68A1C9066CC0030F8D0 /* unit_preview_pane.cpp in Sources */,
91DCA68E1C9066EC0030F8D0 /* unit_recruit.cpp in Sources */,
91DCA6901C9360610030F8D0 /* test_formula_core.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Expand Up @@ -1460,6 +1460,7 @@ if(ENABLE_TESTS)
tests/test_config_cache.cpp
tests/test_filesystem.cpp
tests/test_formula_ai.cpp
tests/test_formula_core.cpp
tests/test_formula_function.cpp
tests/test_image_modifications.cpp
tests/test_lexical_cast.cpp
Expand Down
1 change: 1 addition & 0 deletions src/SConscript
Expand Up @@ -772,6 +772,7 @@ test_sources = Split("""
tests/test_config.cpp
tests/test_filesystem.cpp
tests/test_formula_ai.cpp
tests/test_formula_core.cpp
tests/test_formula_function.cpp
tests/test_image_modifications.cpp
tests/test_lexical_cast.cpp
Expand Down
2 changes: 1 addition & 1 deletion src/ai/formula/ai.cpp
Expand Up @@ -128,7 +128,7 @@ void formula_ai::handle_exception(game_logic::formula_error& e, const std::strin

void formula_ai::display_message(const std::string& msg) const
{
resources::screen->get_chat_manager().add_chat_message(time(NULL), "fai", get_side(), msg,
resources::screen->get_chat_manager().add_chat_message(time(NULL), "wfl", get_side(), msg,
events::chat_handler::MESSAGE_PUBLIC, false);

}
Expand Down
16 changes: 0 additions & 16 deletions src/ai/formula/function_table.cpp
Expand Up @@ -97,20 +97,6 @@ class unit_adapter {
const unit* unit_;
};

class distance_between_function : public function_expression {
public:
explicit distance_between_function(const args_list& args)
: function_expression("distance_between", args, 2, 2)
{}

private:
variant execute(const formula_callable& variables, formula_debugger *fdb) const {
const map_location loc1 = convert_variant<location_callable>(args()[0]->evaluate(variables,add_debug_info(fdb,0,"distance_between:location_A")))->loc();
const map_location loc2 = convert_variant<location_callable>(args()[1]->evaluate(variables,add_debug_info(fdb,1,"distance_between:location_B")))->loc();
return variant(distance_between(loc1, loc2));
}
};


class distance_to_nearest_unowned_village_function : public function_expression {
public:
Expand Down Expand Up @@ -1847,8 +1833,6 @@ expression_ptr ai_function_symbol_table::create_function(const std::string &fn,
return expression_ptr(new close_enemies_function(args, ai_));
} else if(fn == "calculate_outcome") {
return expression_ptr(new calculate_outcome_function(args, ai_));
} else if(fn == "distance_between") {
return expression_ptr(new distance_between_function(args));
} else if(fn == "run_file") {
return expression_ptr(new run_file_function(args, ai_));
} else if(fn == "calculate_map_ownership") {
Expand Down
86 changes: 77 additions & 9 deletions src/callable_objects.cpp
Expand Up @@ -167,7 +167,7 @@ variant unit_callable::get_value(const std::string& key) const
return variant(u_.name());
} else if(key == "usage") {
return variant(u_.usage());
} else if(key == "leader") {
} else if(key == "leader" || key == "canrecruit") {
return variant(u_.can_recruit());
} else if(key == "undead") {
return variant(u_.get_state("not_living") ? 1 : 0);
Expand Down Expand Up @@ -198,14 +198,17 @@ variant unit_callable::get_value(const std::string& key) const
return variant(u_.experience());
} else if(key == "max_experience") {
return variant(u_.max_experience());
} else if(key == "level") {
} else if(key == "level" || key == "full") {
// This allows writing "upkeep == full"
return variant(u_.level());
} else if(key == "total_movement") {
} else if(key == "total_movement" || key == "max_moves") {
return variant(u_.total_movement());
} else if(key == "movement_left") {
} else if(key == "movement_left" || key == "moves") {
return variant(u_.movement_left());
} else if(key == "attacks_left") {
return variant(u_.attacks_left());
} else if(key == "max_attacks") {
return variant(u_.max_attacks());
} else if(key == "traits") {
const std::vector<std::string> traits = u_.get_traits_list();
std::vector<variant> res;
Expand All @@ -218,20 +221,71 @@ variant unit_callable::get_value(const std::string& key) const
res.push_back( variant(*it) );
}
return variant( &res );
} else if(key == "states") {
} else if(key == "extra_recruit") {
const std::vector<std::string> recruits = u_.recruits();
std::vector<variant> res;

if(recruits.empty())
return variant( &res );

for (std::vector<std::string>::const_iterator it = recruits.begin(); it != recruits.end(); ++it)
{
res.push_back( variant(*it) );
}
return variant( &res );
} else if(key == "advances_to") {
const std::vector<std::string> advances = u_.advances_to();
std::vector<variant> res;

if(advances.empty())
return variant( &res );

for (std::vector<std::string>::const_iterator it = advances.begin(); it != advances.end(); ++it)
{
res.push_back( variant(*it) );
}
return variant( &res );
} else if(key == "states" || key == "status") {
const std::map<std::string, std::string>& states_map = u_.get_states();

return convert_map( states_map );
} else if(key == "side") {
return variant(u_.side()-1);
} else if(key == "cost") {
return variant(u_.cost());
} else if(key == "upkeep") {
return variant(u_.upkeep());
} else if(key == "loyal") {
// So we can write "upkeep == loyal"
return variant(0);
} else if(key == "hidden") {
return variant(u_.get_hidden());
} else if(key == "petrified") {
return variant(u_.incapacitated());
} else if(key == "resting") {
return variant(u_.resting());
} else if(key == "role") {
return variant(u_.get_role());
} else if(key == "race") {
return variant(u_.race()->id());
} else if(key == "gender") {
return variant(gender_string(u_.gender()));
} else if(key == "variation") {
return variant(u_.variation());
} else if(key == "zoc") {
return variant(u_.get_emit_zoc());
} else if(key == "alignment") {
return variant(u_.alignment().to_string());
} else if(key == "facing") {
return variant(map_location::write_direction(u_.facing()));
} else if(key == "vars") {
if(u_.formula_manager().formula_vars()) {
return variant(u_.formula_manager().formula_vars().get());
} else {
return variant();
}
} else if(key == "n" || key == "s" || key == "ne" || key == "se" || key == "nw" || key == "sw" || key == "lawful" || key == "neutral" || key == "chaotic" || key == "liminal" || key == "male" || key == "female") {
return variant(key);
} else {
return variant();
}
Expand All @@ -246,7 +300,7 @@ void unit_callable::get_inputs(std::vector<game_logic::formula_input>* inputs) c
inputs->push_back(game_logic::formula_input("id", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("type", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("name", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("leader", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("canrecruit", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("undead", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("traits", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("attacks", FORMULA_READ_ONLY));
Expand All @@ -256,13 +310,27 @@ void unit_callable::get_inputs(std::vector<game_logic::formula_input>* inputs) c
inputs->push_back(game_logic::formula_input("experience", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("max_experience", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("level", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("total_movement", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("movement_left", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("moves", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("max_moves", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("attacks_left", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("max_attacks", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("side", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("states", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("extra_recruit", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("advances_to", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("status", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("cost", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("usage", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("upkeep", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("hidden", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("petrified", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("resting", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("role", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("race", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("gender", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("variation", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("zoc", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("alignment", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("facing", FORMULA_READ_ONLY));
inputs->push_back(game_logic::formula_input("vars", FORMULA_READ_ONLY));
}

Expand Down

0 comments on commit bb510a5

Please sign in to comment.