Updated documentation

Updated documentation

# Gurobify: Gurobify provides an interface to Gurobi from GAP.
#
# Copyright (c) 2017 - 2021 Jesse Lansdown
#
# This Source Code Form is subject to the terms of the Mozilla
# Public License, v. 2.0. If a copy of the MPL was not distributed
# with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

# Declarations
#

DeclareCategory( "IsGurobiModel", IsObject );

GurobiObjectFamily := NewFamily( "GurobiObjectFamily" );

BindGlobal("TheTypeGurobiModel", NewType( GurobiObjectFamily, IsGurobiModel ));

DeclareOperation( "GurobiNewModel",
	[IsList]);

#! @Chapter Using Gurobify
#! @Section Adding And Deleting Constraints
#! @Arguments Model, ConstraintName
#! @Returns true/false
#! @Description
#!	Deletes all constraints with the name ConstraintName
DeclareOperation("GurobiDeleteConstraintsWithName",
	[IsGurobiModel, IsString]);

#! @Chapter Using Gurobify
#! @Section Adding And Deleting Constraints
#! @Arguments Model, ConstraintEquation, ConstraintSense, ConstraintRHSValue[, ConstraintName]
#! @Returns true
#! @Description
#!	Same as below, except that ConstraintRHS value takes an integer value.
DeclareOperation( "GurobiAddConstraint",
	[ IsGurobiModel, IsList, IsString, IsInt, IsString]);

#! @Chapter Using Gurobify
#! @Section Adding And Deleting Constraints
#! @Arguments Model, ConstraintEquation, ConstraintSense, ConstraintRHSValue[, ConstraintName]
#! @Returns true
#! @Description
#!	Adds a constraint to a gurobi model. ConstraintEquation must be a list,
#!	such that each entry is the coefficient (including $0$ coefficents) of the corresponding variable
#! 	in the constraint equation. The ConstraintSense must be one of "<", ">" or "=",
#!	where Gurobi interprets < as <= and > as >=. The ConstraintRHSValue is the value on the
#!	right hand side of the constraint. A constraint may optionally be given a name, which helps to identify
#!	the constraint if it is to be deleted at some point. If no constraint name is given, then a constraint is
#!	simply assigned the name "UnNamedConstraint".
#!	Note that a model must be updated or optimised before any additional constraints become effective.
DeclareOperation( "GurobiAddConstraint",
	[ IsGurobiModel, IsList, IsString, IsFloat, IsString] );

DeclareOperation( "GurobiAddConstraint",
	[ IsGurobiModel, IsList, IsString, IsFloat]);

DeclareOperation( "GurobiAddConstraint",
	[ IsGurobiModel, IsList, IsString, IsInt]);

#! @Chapter Using Gurobify
#! @Section Adding And Deleting Constraints
#! @Arguments Model, ConstraintEquations, ConstraintSenses, ConstraintRHSValues[, ConstraintNames]
#! @Returns true
#! @Description
#!	Add multiple constraints to a model at one time. The arguments (except Model)
#!	are lists, such that the i-th entries of each list determine a single constraint in
#!	the same manner as for the operation GurobiAddConstraint. ConstraintNames is an optional argument,
#!	and must be given for all constraints, or not at all.
#!  If ConstraintSenses and ConstraintRHSValues are all the same, then rather than providing a list, one may
#!  simply provide a single string for the constraint sense and a single float or integer for the right hand side.
DeclareOperation( "GurobiAddMultipleConstraints",
	[ IsGurobiModel, IsList, IsList, IsList, IsList] );

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model
#! @Returns Solution
#! @Description
#!	Display the solution found for a successfuly optimised model. Note that if a solution has not been optimised, is infeasible, or
#!	the optimisation was not completed, then this will return an error. Thus it is advisable to first check the optimisation status.
DeclareOperation( "GurobiSolution",
	[ IsGurobiModel] );

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, TimeLimit
#! @Returns true
#! @Description
#!	Set a time limit for a Gurobi model. Note that TimeLimit should be a float, however an integer value can be given
#!	which will be automatically converted to a float.
DeclareOperation( "GurobiSetTimeLimit",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, BestObjectiveBoundStop
#! @Returns true
#! @Description
#!	Optimisation will terminate if a feasible solution is found with objective value at least as good as BestObjectiveBoundStop.
#!	Note that BestObjectiveBoundStop should be a float, however an integer value can be given
#!	which will be automatically converted to a float.
DeclareOperation( "GurobiSetBestObjectiveBoundStop",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, CutOff
#! @Returns true
#! @Description
#!	Optimisation will terminate if the objective value is worse than CutOff.
#!	Note that CutOff should be a float, an integer value can be given
#!	which will be automatically converted to a float.
DeclareOperation( "GurobiSetCutOff",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Adding And Modifying Objective Functions
#! @Arguments Model
#! @Returns true
#! @Description
#!	Sets the model sense to maximise. When the model is optimised, it will try to maximise the objective function.
DeclareOperation("GurobiMaximiseModel",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Adding And Modifying Objective Functions
#! @Arguments Model
#! @Returns true
#! @Description
#!	Sets the model sense to minimise. When the model is optimised, it will try to minimise the objective function.
DeclareOperation("GurobiMinimiseModel",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Adding And Modifying Objective Functions
#! @Arguments Model, ObjectiveValues
#! @Returns true
#! @Description
#!	Set the objective function for a model. ObjectiveValues is a list of coefficients (including $0$ coefficeints)
#!	corresponding to each of the variables
DeclareOperation( "GurobiSetObjectiveFunction",
	[ IsGurobiModel, IsList] );

#! @Chapter Using Gurobify
#! @Section Adding And Modifying Objective Functions
#! @Arguments Model
#! @Returns List of coefficients of the objective function
#! @Description
#!	View the objective function for a model.
DeclareOperation( "GurobiObjectiveFunction",
	[ IsGurobiModel] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Number of variables
#! @Description
#!	Returns the number of variables in the model.
DeclareOperation("GurobiNumberOfVariables",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Number of linear constraints
#! @Description
#!	Returns the number of linear constraints in the model.
DeclareOperation("GurobiNumberOfConstraints",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model
#! @Returns objective value
#! @Description
#!	Returns the objective value of the current solution.
DeclareOperation("GurobiObjectiveValue",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns objective bound
#! @Description
#!	Returns the best known bound on the objective value of the model.
DeclareOperation("GurobiObjectiveBound",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns run time of optimisation
#! @Description
#!	Returns the wall clock runtime in seconds for the most recent optimisation.
DeclareOperation("GurobiRunTime",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model
#! @Returns optimisation status code
#! @Description
#!	Returns the optimisation status code of the most recent optimisation. Refer to the Gurobi documentation for more
#!	on the optimisation statuses, or alternatively refer to the Appendix of this manual.
DeclareOperation("GurobiOptimisationStatus",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns numeric focus
#! @Description
#!	Returns the numeric focus value of the model. The numeric focus is a value in the set [0,1,2,3]. A numeric focus of $0$ sets the
#!	numeric focus automatically, preferencing speed. Values between 1 and 3 increase the care taken in computations
#!	as the value increases, but also take longer. The default value is 0.
DeclareOperation("GurobiNumericFocus",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, NumericFocus
#! @Returns true
#! @Description
#!	Set the numeric focus for a model. Numeric focus must be in the set [0,1,2,3]. A numeric focus of $0$ sets the
#!	numeric focus automatically, preferencing speed. Values between 1 and 3 increase the care taken in computations
#!	as the value increases, but also take longer. The default value is 0.
DeclareOperation( "GurobiSetNumericFocus",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns time limit
#! @Description
#!	Returns the time limit for the model. The default value is infinity.
DeclareOperation("GurobiTimeLimit",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns cutoff value
#! @Description
#!	Returns the cutoff value for the model. Optimisation will terminate if the objective value is worse than CutOff.
#!	The default value is infinity for minimisation, and negative infinity for maximisation.
DeclareOperation("GurobiCutOff",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns best objective bound limit value
#! @Description
#!	Returns the best objective bound limit value for the model.
#!	Optimisation will terminate if a feasible solution is found with objective value at least as good as the best objective bound.
#!	The default value is negative infinity.
DeclareOperation("GurobiBestObjectiveBoundStop",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, MIPFocus
#! @Returns true
#! @Description
#!	Set the MIP focus for a model. The mip focus must be in the set [0,1,2,3], and the default value is 0.
#!	The MIP focus alows you to prioritise finding solutions or proving their optimality. See the Gurobi
#!	documentation for more information.
DeclareOperation( "GurobiSetMIPFocus",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns MIP focus
#! @Description
#!	Returns the MIP focus value for the model. The mip focus must be in the set [0,1,2,3], and the default value is 0.
#!	The MIP focus alows you to prioritise finding solutions or proving their optimality.  See the Gurobi
#!	documentation for more information.
DeclareOperation("GurobiMIPFocus",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, BestBdStop
#! @Returns true
#! @Description
#!	Set the best bound stopping value for a model. Terminates optimisation as soon as the value
#!	of the best bound is determined to be at least as good as the best bound stopping value. Default value is infinity.
DeclareOperation( "GurobiSetBestBoundStop",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Best bound stopping value
#! @Description
#!	Returns the best bound stopping value for the model. Optimisation terminates as soon as the value
#!	of the best bound is determined to be at least as good as the best bound stopping value. Default value is infinity.
DeclareOperation("GurobiBestBoundStop",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, BestBdStop
#! @Returns true
#! @Description
#!	Set the limit for the maximum number of MIP solutions to find.
#!	Default value is $2,000,000,000$.
DeclareOperation( "GurobiSetSolutionLimit",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns solution limit value
#! @Description
#!	Returns the solution limit value for the model. This value limits the maximum number of MIP solutions that will be found.
#!	Default value is $2,000,000,000$.
DeclareOperation("GurobiSolutionLimit",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, IterationLimit
#! @Returns true
#! @Description
#!	Set the limit for the maximum number of simplex iterations performed. Default value is infinity.
DeclareOperation( "GurobiSetIterationLimit",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Iteration limit
#! @Description
#!	Returns the iteration limit value for the model, which limits the number of simplex iterations performed during optimisation.
#!	Default value is infinity.
DeclareOperation("GurobiIterationLimit",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, NodeLimit
#! @Returns true
#! @Description
#!	Set the limit for the maximum number of MIP nodes explored.
#!	The default value is infinity.
DeclareOperation( "GurobiSetNodeLimit",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Node limit
#! @Description
#!	Returns the node limit value for the model, which limits the number of MIP nodes explored during optimisation.
#!	The default value is infinity.
DeclareOperation("GurobiNodeLimit",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns
#! @Description
#!	Returns the names of the variables in the model. This list acts as the index set for
#!	any lists of variable coefficients, such as in GurobiAddConstraint or GurobiSetObjectiveFunction.
DeclareOperation("GurobiVariableNames",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Creating Or Reading A Model
#! @Arguments VariableTypes[, VariableNames]
#! @Returns A Gurobi model
#! @Description
#!  Creates a gurobi model with variables defined by VariableTypes. VariableTypes must be a list of strings,
#!	where each entry is the type of the corresponding variable.
#!  Accepted variable types are "CONTINUOUS", "BINARY", "INTEGER", "SEMICONT", or "SEMIINT". The variable types are not case sensitive.
#!  Refer to the Gurobi documentation for more information on the variable types.
#!  Optionally takes the names of the variables as a list of strings.
#!  If all variables are of the same type, then you may instead give arguments n, v, where n is the number
#!  of variables and v is the variable type.
DeclareOperation( "GurobiNewModel",
	[IsList, IsList]);

#! @Chapter Using Gurobify
#! @Section Creating Or Reading A Model
#! @Arguments Model, VariableNames
#! @Returns true
#! @Description
#!  Assigns each variable a new name from a list of names. The names must be strings.
DeclareOperation("GurobiSetVariableNames",
		[IsGurobiModel, IsList]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, Switch
#! @Returns true
#! @Description
#!  Turns console logging on or off. If Switch is true the output of Gurobi will be printed to the screen,
#!	and if it is false the output will be supressed. The default for Gurobify is to supress the output.
DeclareOperation("GurobiSetLogToConsole",
		[IsGurobiModel, IsBool]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns true/false
#! @Description
#!	Returns true if the Gurobi output is set to display to the screen, and false if the output is supressed.
#!	Gurobify suppresses the output by default.
DeclareOperation("GurobiLogToConsole",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model, Size
#! @Returns Set of all solutions.
#! @Description
#!	This function finds all possible solutions of a given size, for a model with only binary variables.
#!	Takes a Gurobi model and repeatedly optimises it, each time adding the previous solution as a
#!	constraint so that it isn't found again. This continues until all solutions are found, and
#!	then they are returned as a set. During the process the number of found solutions is displayed.
#!	Note:
#!		- Only for models where every variable is a binary variable.
#!		- Only finds solution sets of a given size.
DeclareOperation("GurobiFindAllBinarySolutions",
	[IsGurobiModel, IsPosInt]);

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model, Size, Group
#! @Returns Set of all solutions.
#! @Description
#!	This function finds all possible solutions of a given size, for a model with only binary variables.
#!	Same as above, except that it also takes a permutation group acting on the index set of variables.
#!	Instead of finding all solutions directly, the group is used to find the orbit of each new
#!	solution, and these are then all returned at the end, and used as constraints until then.
#!	An option value may also be given which will only return the representatives of each orbit of the
#!	solutions. Hence it returns all the unique solutions up to equivalence under the group.
#!	This saves on memory, and the remaing solutions may be refound by generating the
#!	orbit under the group. To invoke this option place a colon after the group argument and then put
#!	representatives:=true so for example GurobiFindAllSolutions(model, size, gp : representatives:=true);
DeclareOperation("GurobiFindAllBinarySolutions",
	[IsGurobiModel, IsPosInt, IsGroup]);

#! @Chapter Using Gurobify
#! @Section Additional Functionality
#! @Arguments IndexSet, NumberOfIndices
#! @Returns Characterisitc vector
#! @Description
#!	Takes a list of integers which form a subset of the set [1 .. n], where n is the second argument,
#!	and converts the set of indices to its characteristic vector. For example, if n = 5, the set
#!	[1,3] would be converted to [1, 0, 1, 0, 0]. It is useful to be able to convert between the two,
#!	since Gurobify always takes the characteristic vector (for example when taking constraints),
#!	yet the set of indices is generally more helpful for the user.
DeclareOperation("IndexSetToCharacteristicVector",
	[IsList, IsPosInt]);

#! @Chapter Using Gurobify
#! @Section Additional Functionality
#! @Arguments CharacteristicVector
#! @Returns Characterisitc vector
#! @Description
#!	Takes a characteristic vector and returns the set of indices corresponding to it. This reverses the
#!	process which occurs with IndexSetToCharacteristicVector. It is particularly useful to convert the
#!	output of a Gurobi solution back in terms of the variables. For example, the characteristic vector
#!	[1, 0, 1, 0, 0] would return the index set [1,3]. Note that since the function expects a characteristic
#!	vector it doesn't account for any weightings, and is only interested in whether or not the corresponding
#!	index is present, and as such it rounds each entry to the nearest integer and checks that it is non-zero.
#!	Hence it is particularly suitable for use with binary variables.
DeclareOperation("CharacteristicVectorToIndexSet",
	[IsList]);

#! @Chapter Using Gurobify
#! @Section Additional Functionality
#! @Arguments Subset, FullSet
#! @Returns Characterisitc vector
#! @Description
#!	Takes a subset of some set, and returns the characteristic vector where the entries of the characteristic
#!	vector are indexed by the full set. For example, the subset ["c"] of ["a", "c", "n", "q"] would give the
#!	characteristic vector [0, 1, 0, 0]. This removes the need to first find the index set of the subset.
DeclareOperation("SubsetToCharacteristicVector",
	[IsList, IsList]);

#! @Chapter Using Gurobify
#! @Section Additional Functionality
#! @Arguments CharacteristicVector
#! @Returns Characterisitc vector
#! @Description
#!	Takes a characteristic vector and some set which it takes to be indexing the entries of the characteristic
#!	vector. It then returns the subset of the full set corresponding to the non-zero entries of the characteristic
#!	vector. This is the reverse process to SubsetToCharacteristicVector. Note again that the characteristic vector
#!	is rounded to an integer before being compared to 0. As an example, the characteristic vector [0, 1, 0, 0] with
#!	the set ["a", "c", "n", "q"] would return ["c"]. This removes the need to first return an index set before
#!	finding the subset.
DeclareOperation("CharacteristicVectorToSubset",
	[IsList, IsList]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns
#! @Description
#!	Returns the types of the variables in the model.
DeclareOperation("GurobiVariableTypes",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, MethodType
#! @Returns true
#! @Description
#!	Set the method used to solve a model.
#!	-1=automatic (this is the default),
#!	0=primal simplex,
#!	1=dual simplex,
#!	2=barrier,
#!	3=concurrent,
#!	4=deterministic concurrent,
#!	5=deterministic concurrent simplex.
#!	See the Gurobi documentation for more details.
DeclareOperation( "GurobiSetMethod",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns MethodType
#! @Description
#!	Returns the method used to solve a model. See the Gurobi documentation for more details.
DeclareOperation("GurobiMethod",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, ThreadCount
#! @Returns true
#! @Description
#!	Set the number of threads Gurobi is allowed to use. The default value is 0,
#!	which will use as many cores as it wants. See the Gurobi documentation for more details.
DeclareOperation( "GurobiSetThreads",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns ThreadCount
#! @Description
#!	Returns number of threads Gurobi is allowed to use. See the Gurobi documentation for more details.
DeclareOperation("GurobiThreads",
	[IsGurobiModel]);

# Gurobify: Gurobify provides an interface to Gurobi from GAP.
#
# Copyright (c) 2017 - 2021 Jesse Lansdown
#
# This Source Code Form is subject to the terms of the Mozilla
# Public License, v. 2.0. If a copy of the MPL was not distributed
# with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

# Declarations
#

DeclareCategory( "IsGurobiModel", IsObject );

GurobiObjectFamily := NewFamily( "GurobiObjectFamily" );

BindGlobal("TheTypeGurobiModel", NewType( GurobiObjectFamily, IsGurobiModel ));

DeclareOperation( "GurobiNewModel",
	[IsList]);

#! @Chapter Using Gurobify
#! @Section Adding And Deleting Constraints
#! @Arguments Model, ConstraintName
#! @Returns true/false
#! @Description
#!	Deletes all constraints with the name ConstraintName
DeclareOperation("GurobiDeleteConstraintsWithName",
	[IsGurobiModel, IsString]);

#! @Chapter Using Gurobify
#! @Section Adding And Deleting Constraints
#! @Arguments Model, ConstraintEquation, ConstraintSense, ConstraintRHSValue[, ConstraintName]
#! @Returns true
#! @Description
#!	Same as below, except that ConstraintRHS value takes an integer value.
DeclareOperation( "GurobiAddConstraint",
	[ IsGurobiModel, IsList, IsString, IsInt, IsString]);

#! @Chapter Using Gurobify
#! @Section Adding And Deleting Constraints
#! @Arguments Model, ConstraintEquation, ConstraintSense, ConstraintRHSValue[, ConstraintName]
#! @Returns true
#! @Description
#!	Adds a constraint to a gurobi model. ConstraintEquation must be a list,
#!	such that each entry is the coefficient (including $0$ coefficents) of the corresponding variable
#! 	in the constraint equation. The ConstraintSense must be one of "<", ">" or "=",
#!	where Gurobi interprets < as <= and > as >=. The ConstraintRHSValue is the value on the
#!	right hand side of the constraint. A constraint may optionally be given a name, which helps to identify
#!	the constraint if it is to be deleted at some point. If no constraint name is given, then a constraint is
#!	simply assigned the name "UnNamedConstraint".
#!	Note that a model must be updated or optimised before any additional constraints become effective.
DeclareOperation( "GurobiAddConstraint",
	[ IsGurobiModel, IsList, IsString, IsFloat, IsString] );

DeclareOperation( "GurobiAddConstraint",
	[ IsGurobiModel, IsList, IsString, IsFloat]);

DeclareOperation( "GurobiAddConstraint",
	[ IsGurobiModel, IsList, IsString, IsInt]);

#! @Chapter Using Gurobify
#! @Section Adding And Deleting Constraints
#! @Arguments Model, ConstraintEquations, ConstraintSenses, ConstraintRHSValues[, ConstraintNames]
#! @Returns true
#! @Description
#!	Add multiple constraints to a model at one time. The arguments (except Model)
#!	are lists, such that the i-th entries of each list determine a single constraint in
#!	the same manner as for the operation GurobiAddConstraint. ConstraintNames is an optional argument,
#!	and must be given for all constraints, or not at all.
DeclareOperation( "GurobiAddMultipleConstraints",
	[ IsGurobiModel, IsList, IsList, IsList, IsList] );

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model
#! @Returns Solution
#! @Description
#!	Display the solution found for a successfuly optimised model. Note that if a solution has not been optimised, is infeasible, or
#!	the optimisation was not completed, then this will return an error. Thus it is advisable to first check the optimisation status.
DeclareOperation( "GurobiSolution",
	[ IsGurobiModel] );

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, TimeLimit
#! @Returns true
#! @Description
#!	Set a time limit for a Gurobi model. Note that TimeLimit should be a float, however an integer value can be given
#!	which will be automatically converted to a float.
DeclareOperation( "GurobiSetTimeLimit",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, BestObjectiveBoundStop
#! @Returns true
#! @Description
#!	Optimisation will terminate if a feasible solution is found with objective value at least as good as BestObjectiveBoundStop.
#!	Note that BestObjectiveBoundStop should be a float, however an integer value can be given
#!	which will be automatically converted to a float.
DeclareOperation( "GurobiSetBestObjectiveBoundStop",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, CutOff
#! @Returns true
#! @Description
#!	Optimisation will terminate if the objective value is worse than CutOff.
#!	Note that CutOff should be a float, an integer value can be given
#!	which will be automatically converted to a float.
DeclareOperation( "GurobiSetCutOff",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Adding And Modifying Objective Functions
#! @Arguments Model
#! @Returns true
#! @Description
#!	Sets the model sense to maximise. When the model is optimised, it will try to maximise the objective function.
DeclareOperation("GurobiMaximiseModel",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Adding And Modifying Objective Functions
#! @Arguments Model
#! @Returns true
#! @Description
#!	Sets the model sense to minimise. When the model is optimised, it will try to minimise the objective function.
DeclareOperation("GurobiMinimiseModel",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Adding And Modifying Objective Functions
#! @Arguments Model, ObjectiveValues
#! @Returns true
#! @Description
#!	Set the objective function for a model. ObjectiveValues is a list of coefficients (including $0$ coefficeints)
#!	corresponding to each of the variables
DeclareOperation( "GurobiSetObjectiveFunction",
	[ IsGurobiModel, IsList] );

#! @Chapter Using Gurobify
#! @Section Adding And Modifying Objective Functions
#! @Arguments Model
#! @Returns List of coefficients of the objective function
#! @Description
#!	View the objective function for a model.
DeclareOperation( "GurobiObjectiveFunction",
	[ IsGurobiModel] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Number of variables
#! @Description
#!	Returns the number of variables in the model.
DeclareOperation("GurobiNumberOfVariables",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Number of linear constraints
#! @Description
#!	Returns the number of linear constraints in the model.
DeclareOperation("GurobiNumberOfConstraints",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model
#! @Returns objective value
#! @Description
#!	Returns the objective value of the current solution.
DeclareOperation("GurobiObjectiveValue",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns objective bound
#! @Description
#!	Returns the best known bound on the objective value of the model.
DeclareOperation("GurobiObjectiveBound",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns run time of optimisation
#! @Description
#!	Returns the wall clock runtime in seconds for the most recent optimisation.
DeclareOperation("GurobiRunTime",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model
#! @Returns optimisation status code
#! @Description
#!	Returns the optimisation status code of the most recent optimisation. Refer to the Gurobi documentation for more
#!	on the optimisation statuses, or alternatively refer to the Appendix of this manual.
DeclareOperation("GurobiOptimisationStatus",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns numeric focus
#! @Description
#!	Returns the numeric focus value of the model. The numeric focus is a value in the set [0,1,2,3]. A numeric focus of $0$ sets the
#!	numeric focus automatically, preferencing speed. Values between 1 and 3 increase the care taken in computations
#!	as the value increases, but also take longer. The default value is 0.
DeclareOperation("GurobiNumericFocus",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, NumericFocus
#! @Returns true
#! @Description
#!	Set the numeric focus for a model. Numeric focus must be in the set [0,1,2,3]. A numeric focus of $0$ sets the
#!	numeric focus automatically, preferencing speed. Values between 1 and 3 increase the care taken in computations
#!	as the value increases, but also take longer. The default value is 0.
DeclareOperation( "GurobiSetNumericFocus",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns time limit
#! @Description
#!	Returns the time limit for the model. The default value is infinity.
DeclareOperation("GurobiTimeLimit",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns cutoff value
#! @Description
#!	Returns the cutoff value for the model. Optimisation will terminate if the objective value is worse than CutOff.
#!	The default value is infinity for minimisation, and negative infinity for maximisation.
DeclareOperation("GurobiCutOff",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns best objective bound limit value
#! @Description
#!	Returns the best objective bound limit value for the model.
#!	Optimisation will terminate if a feasible solution is found with objective value at least as good as the best objective bound.
#!	The default value is negative infinity.
DeclareOperation("GurobiBestObjectiveBoundStop",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, MIPFocus
#! @Returns true
#! @Description
#!	Set the MIP focus for a model. The mip focus must be in the set [0,1,2,3], and the default value is 0.
#!	The MIP focus alows you to prioritise finding solutions or proving their optimality. See the Gurobi
#!	documentation for more information.
DeclareOperation( "GurobiSetMIPFocus",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns MIP focus
#! @Description
#!	Returns the MIP focus value for the model. The mip focus must be in the set [0,1,2,3], and the default value is 0.
#!	The MIP focus alows you to prioritise finding solutions or proving their optimality.  See the Gurobi
#!	documentation for more information.
DeclareOperation("GurobiMIPFocus",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, BestBdStop
#! @Returns true
#! @Description
#!	Set the best bound stopping value for a model. Terminates optimisation as soon as the value
#!	of the best bound is determined to be at least as good as the best bound stopping value. Default value is infinity.
DeclareOperation( "GurobiSetBestBoundStop",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Best bound stopping value
#! @Description
#!	Returns the best bound stopping value for the model. Optimisation terminates as soon as the value
#!	of the best bound is determined to be at least as good as the best bound stopping value. Default value is infinity.
DeclareOperation("GurobiBestBoundStop",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, BestBdStop
#! @Returns true
#! @Description
#!	Set the limit for the maximum number of MIP solutions to find.
#!	Default value is $2,000,000,000$.
DeclareOperation( "GurobiSetSolutionLimit",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns solution limit value
#! @Description
#!	Returns the solution limit value for the model. This value limits the maximum number of MIP solutions that will be found.
#!	Default value is $2,000,000,000$.
DeclareOperation("GurobiSolutionLimit",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, IterationLimit
#! @Returns true
#! @Description
#!	Set the limit for the maximum number of simplex iterations performed. Default value is infinity.
DeclareOperation( "GurobiSetIterationLimit",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Iteration limit
#! @Description
#!	Returns the iteration limit value for the model, which limits the number of simplex iterations performed during optimisation.
#!	Default value is infinity.
DeclareOperation("GurobiIterationLimit",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, NodeLimit
#! @Returns true
#! @Description
#!	Set the limit for the maximum number of MIP nodes explored.
#!	The default value is infinity.
DeclareOperation( "GurobiSetNodeLimit",
	[ IsGurobiModel, IsFloat] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns Node limit
#! @Description
#!	Returns the node limit value for the model, which limits the number of MIP nodes explored during optimisation.
#!	The default value is infinity.
DeclareOperation("GurobiNodeLimit",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns
#! @Description
#!	Returns the names of the variables in the model. This list acts as the index set for
#!	any lists of variable coefficients, such as in GurobiAddConstraint or GurobiSetObjectiveFunction.
DeclareOperation("GurobiVariableNames",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Creating Or Reading A Model
#! @Arguments VariableTypes[, VariableNames]
#! @Returns A Gurobi model
#! @Description
#!  Creates a gurobi model with variables defined by VariableTypes. VariableTypes must be a list of strings,
#!	where each entry is the type of the corresponding variable.
#!  Accepted variable types are "CONTINUOUS", "BINARY", "INTEGER", "SEMICONT", or "SEMIINT". The variable types are not case sensitive.
#!  Refer to the Gurobi documentation for more information on the variable types.
#!  Optionally takes the names of the variables as a list of strings.
DeclareOperation( "GurobiNewModel",
	[IsList, IsList]);

#! @Chapter Using Gurobify
#! @Section Creating Or Reading A Model
#! @Arguments Model, VariableNames
#! @Returns true
#! @Description
#!  Assigns each variable a new name from a list of names. The names must be strings.
DeclareOperation("GurobiSetVariableNames",
		[IsGurobiModel, IsList]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, Switch
#! @Returns true
#! @Description
#!  Turns console logging on or off. If Switch is true the output of Gurobi will be printed to the screen,
#!	and if it is false the output will be supressed. The default for Gurobify is to supress the output.
DeclareOperation("GurobiSetLogToConsole",
		[IsGurobiModel, IsBool]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns true/false
#! @Description
#!	Returns true if the Gurobi output is set to display to the screen, and false if the output is supressed.
#!	Gurobify suppresses the output by default.
DeclareOperation("GurobiLogToConsole",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model, Size
#! @Returns Set of all solutions.
#! @Description
#!	This function finds all possible solutions of a given size, for a model with only binary variables.
#!	Takes a Gurobi model and repeatedly optimises it, each time adding the previous solution as a
#!	constraint so that it isn't found again. This continues until all solutions are found, and
#!	then they are returned as a set. During the process the number of found solutions is displayed.
#!	Note:
#!		- Only for models where every variable is a binary variable.
#!		- Only finds solution sets of a given size.
DeclareOperation("GurobiFindAllBinarySolutions",
	[IsGurobiModel, IsPosInt]);

#! @Chapter Using Gurobify
#! @Section Optimising A Model
#! @Arguments Model, Size, Group
#! @Returns Set of all solutions.
#! @Description
#!	This function finds all possible solutions of a given size, for a model with only binary variables.
#!	Same as above, except that it also takes a permutation group acting on the index set of variables.
#!	Instead of finding all solutions directly, the group is used to find the orbit of each new
#!	solution, and these are then all returned at the end, and used as constraints until then.
#!	An option value may also be given which will only return the representatives of each orbit of the
#!	solutions. Hence it returns all the unique solutions up to equivalence under the group.
#!	This saves on memory, and the remaing solutions may be refound by generating the
#!	orbit under the group. To invoke this option place a colon after the group argument and then put
#!	representatives:=true so for example GurobiFindAllSolutions(model, size, gp : representatives:=true);
DeclareOperation("GurobiFindAllBinarySolutions",
	[IsGurobiModel, IsPosInt, IsGroup]);

#! @Chapter Using Gurobify
#! @Section Additional Functionality
#! @Arguments IndexSet, NumberOfIndices
#! @Returns Characterisitc vector
#! @Description
#!	Takes a list of integers which form a subset of the set [1 .. n], where n is the second argument,
#!	and converts the set of indices to its characteristic vector. For example, if n = 5, the set
#!	[1,3] would be converted to [1, 0, 1, 0, 0]. It is useful to be able to convert between the two,
#!	since Gurobify always takes the characteristic vector (for example when taking constraints),
#!	yet the set of indices is generally more helpful for the user.
DeclareOperation("IndexSetToCharacteristicVector",
	[IsList, IsPosInt]);

#! @Chapter Using Gurobify
#! @Section Additional Functionality
#! @Arguments CharacteristicVector
#! @Returns Characterisitc vector
#! @Description
#!	Takes a characteristic vector and returns the set of indices corresponding to it. This reverses the
#!	process which occurs with IndexSetToCharacteristicVector. It is particularly useful to convert the
#!	output of a Gurobi solution back in terms of the variables. For example, the characteristic vector
#!	[1, 0, 1, 0, 0] would return the index set [1,3]. Note that since the function expects a characteristic
#!	vector it doesn't account for any weightings, and is only interested in whether or not the corresponding
#!	index is present, and as such it rounds each entry to the nearest integer and checks that it is non-zero.
#!	Hence it is particularly suitable for use with binary variables.
DeclareOperation("CharacteristicVectorToIndexSet",
	[IsList]);

#! @Chapter Using Gurobify
#! @Section Additional Functionality
#! @Arguments Subset, FullSet
#! @Returns Characterisitc vector
#! @Description
#!	Takes a subset of some set, and returns the characteristic vector where the entries of the characteristic
#!	vector are indexed by the full set. For example, the subset ["c"] of ["a", "c", "n", "q"] would give the
#!	characteristic vector [0, 1, 0, 0]. This removes the need to first find the index set of the subset.
DeclareOperation("SubsetToCharacteristicVector",
	[IsList, IsList]);

#! @Chapter Using Gurobify
#! @Section Additional Functionality
#! @Arguments CharacteristicVector
#! @Returns Characterisitc vector
#! @Description
#!	Takes a characteristic vector and some set which it takes to be indexing the entries of the characteristic
#!	vector. It then returns the subset of the full set corresponding to the non-zero entries of the characteristic
#!	vector. This is the reverse process to SubsetToCharacteristicVector. Note again that the characteristic vector
#!	is rounded to an integer before being compared to 0. As an example, the characteristic vector [0, 1, 0, 0] with
#!	the set ["a", "c", "n", "q"] would return ["c"]. This removes the need to first return an index set before
#!	finding the subset.
DeclareOperation("CharacteristicVectorToSubset",
	[IsList, IsList]);

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns
#! @Description
#!	Returns the types of the variables in the model.
DeclareOperation("GurobiVariableTypes",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, MethodType
#! @Returns true
#! @Description
#!	Set the method used to solve a model.
#!	-1=automatic (this is the default),
#!	0=primal simplex,
#!	1=dual simplex,
#!	2=barrier,
#!	3=concurrent,
#!	4=deterministic concurrent,
#!	5=deterministic concurrent simplex.
#!	See the Gurobi documentation for more details.
DeclareOperation( "GurobiSetMethod",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns MethodType
#! @Description
#!	Returns the method used to solve a model. See the Gurobi documentation for more details.
DeclareOperation("GurobiMethod",
	[IsGurobiModel]);

#! @Chapter Using Gurobify
#! @Section Modifying Attributes And Parameters
#! @Arguments Model, ThreadCount
#! @Returns true
#! @Description
#!	Set the number of threads Gurobi is allowed to use. The default value is 0,
#!	which will use as many cores as it wants. See the Gurobi documentation for more details.
DeclareOperation( "GurobiSetThreads",
	[ IsGurobiModel, IsInt] );

#! @Chapter Using Gurobify
#! @Section Querying Attributes And Parameters
#! @Arguments Model
#! @Returns ThreadCount
#! @Description
#!	Returns number of threads Gurobi is allowed to use. See the Gurobi documentation for more details.
DeclareOperation("GurobiThreads",
	[IsGurobiModel]);


better dialog logic. pause game. work during pause.

extends Spatial

var puzzleScn = preload("res://puzzle.scn")
var puzzle
var puzzleMan

var gridMan
var prevBlock = null

var fileDialog = load("res://fileDialog.scn").instance()
var gui = [
	["status", Label.new()], # plz keep me as first element, referenced as gui[0] later
	["save_pzl", Button.new()],
	["load_pzl", Button.new()],
	["remove_layer", Button.new()],
	["random_layer", Button.new()],
	# THESE SHOULD BE Options instead of left, label, right
	["class_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["LZR", "WILD", "PAIR", "GOAL"],
		value="PAIR"
	}],
	["color_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=preload("res://scripts/PuzzleManager.gd").blockColors,
		value="Red"
	}],
	["action_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["Add", "Remove", "Replace"],
		value="Add"
	}],
	["test_pzl", Button.new()],
]

func shouldAddNeighbor():
	return true

func shouldReplaceSelf():
	return false

func shouldRemoveSelf():
	return false

func newPickledBlock():
	var b = puzzleMan.PickledBlock.new()\
		.setName(gridMan.shape.keys().size())\
	if prevBlock != null:
		b.setPairName(prevBlock.name)
		gridMan.get_node(prevBlock.toNode().name).setPairName(b.name)
		prevBlock = null
		gui[0][1].set_text("STATUS: ERROR, ADD PAIR")
	else:
		prevBlock = b
		gui[0][1].set_text("STATUS: NOMINAL")
	return b

func updateToggle(togg, increase):
	if increase:
		var next_ix = togg.values.find(togg.value) + 1
		if (next_ix >= togg.values.size()):
			next_ix = 0
		togg.value = togg.values[next_ix]
	else:
		var next_ix = togg.values.find(togg.value) - 1
		if (next_ix < 0):
			next_ix = togg.values.size() - 1
		togg.value =  togg.values[next_ix]
	togg.label.set_text(togg.value)

func showFileDialog(loadInstead=false):
	get_tree().set_pause(true)
	fileDialog.popup_centered()

	if loadInstead:
		fileDialog.connect("confirmed", self, "puzzleLoad")
	else:
		fileDialog.connect("confirmed", self, "puzzleSave")

func puzzleSave():
	print("SAVING TO ", fileDialog.get_current_file() )
	get_tree().set_pause(false)

func puzzleLoad():
	print("LOADING FROM ", fileDialog.get_current_file() )
	get_tree().set_pause(false)

func _ready():
	puzzle = puzzleScn.instance()
	gridMan = puzzle.get_node("GridView/GridMan")
	puzzle.mainPuzzle = false

	# hide and disable timer
	puzzle.time.on = false;
	puzzle.time.val = ''

	puzzle.set_as_toplevel(true)

	add_child(puzzle)

	print(puzzle.get_tree() == get_tree())
	puzzleMan = puzzle.puzzleMan

	set_process_input(true)

	var theme = preload("res://themes/MainTheme.thm")

	fileDialog.set_title("Select Puzzle Filename")
	fileDialog.set_access(FileDialog.ACCESS_USERDATA)

	# MainTheme leaks on bottom
	var dialogTheme = Theme.new()
	dialogTheme.copy_default_theme()
	fileDialog.set_theme(dialogTheme)

	# work even if game is paused
	fileDialog.set_pause_mode(PAUSE_MODE_PROCESS)

	# unpause if user cancels
	fileDialog.connect("popup_hide", get_tree(), "set_pause", [false])
	fileDialog.hide()
	add_child(fileDialog)

	var y = 0
	for control in gui:
		y += 45
		if control[0].rfind('_toggle') > 0:
			var togg = control[1]
			for cont in togg.keys():
				if not cont.begins_with('value'):
					var e = togg[cont]
					e.set_pos(Vector2(10, y))
					e.set_theme(theme)
					add_child(e)

			togg.left.set_text("<")
			togg.left.connect('pressed', self, 'updateToggle', [togg, false])

			togg.label.set_pos(Vector2(90, y + 4))
			togg.label.set_text(togg.value)

			togg.right.set_pos(Vector2(55, y))
			togg.right.set_text(">")
			togg.right.connect('pressed', self, 'updateToggle', [togg, true])

		else:
			var e = control[1]
			e.set_pos(Vector2(10, y))
			e.set_theme(theme)
			if e extends Button:
				e.set_text(control[0])
			if control[0] == 'save_pzl' :
				e.connect('pressed', self, 'showFileDialog')
			if control[0] == 'load_pzl' :
				e.connect('pressed', self, 'showFileDialog', [true])
			if control[0] == 'status':
				control[1].set_text('STATUS: NOMINAL')
			add_child(e)

extends Spatial

var puzzleScn = preload("res://puzzle.scn")
var puzzle
var puzzleMan

var gridMan
var prevBlock = null

var fileDialog = load("res://fileDialog.scn").instance()
var gui = [
	["status", Label.new()], # plz keep me as first element, referenced as gui[0] later
	["save_pzl", Button.new()],
	["load_pzl", Button.new()],
	["remove_layer", Button.new()],
	["random_layer", Button.new()],
	# THESE SHOULD BE Options instead of left, label, right
	["class_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["LZR", "WILD", "PAIR", "GOAL"],
		value="PAIR"
	}],
	["color_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=preload("res://scripts/PuzzleManager.gd").blockColors,
		value="Red"
	}],
	["action_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["Add", "Remove", "Replace"],
		value="Add"
	}],
	["test_pzl", Button.new()],
]

func shouldAddNeighbor():
	return true

func shouldReplaceSelf():
	return false

func shouldRemoveSelf():
	return false

func newPickledBlock():
	var b = puzzleMan.PickledBlock.new()\
		.setName(gridMan.shape.keys().size())\
	if prevBlock != null:
		b.setPairName(prevBlock.name)
		gridMan.get_node(prevBlock.toNode().name).setPairName(b.name)
		prevBlock = null
		gui[0][1].set_text("STATUS: ERROR, ADD PAIR")
	else:
		prevBlock = b
		gui[0][1].set_text("STATUS: NOMINAL")
	return b

func updateToggle(togg, increase):
	if increase:
		var next_ix = togg.values.find(togg.value) + 1
		if (next_ix >= togg.values.size()):
			next_ix = 0
		togg.value = togg.values[next_ix]
	else:
		var next_ix = togg.values.find(togg.value) - 1
		if (next_ix < 0):
			next_ix = togg.values.size() - 1
		togg.value =  togg.values[next_ix]
	togg.label.set_text(togg.value)

func puzzleSave(loadInstead=false):
	fileDialog.popup_centered()

func _ready():
	puzzle = puzzleScn.instance()
	gridMan = puzzle.get_node("GridView/GridMan")
	puzzle.mainPuzzle = false
	puzzle.time.on = false;
	puzzle.time.val = ''
	puzzle.set_as_toplevel(true)

	add_child(puzzle)
	puzzleMan = puzzle.puzzleMan

	set_process_input(true)

	var theme = preload("res://themes/MainTheme.thm")
	var dialogTheme = Theme.new()
	dialogTheme.copy_default_theme()

	fileDialog.set_title("Select Puzzle Filename")
	fileDialog.set_access(FileDialog.ACCESS_USERDATA)
	fileDialog.set_theme(dialogTheme)
	fileDialog.hide()
	add_child(fileDialog)

	var y = 0
	for control in gui:
		y += 45
		if control[0].rfind('_toggle') > 0:
			var togg = control[1]
			for cont in togg.keys():
				if not cont.begins_with('value'):
					var e = togg[cont]
					e.set_pos(Vector2(10, y))
					e.set_theme(theme)
					add_child(e)

			togg.left.set_text("<")
			togg.left.connect('pressed', self, 'updateToggle', [togg, false])

			togg.label.set_pos(Vector2(90, y + 4))
			togg.label.set_text(togg.value)

			togg.right.set_pos(Vector2(55, y))
			togg.right.set_text(">")
			togg.right.connect('pressed', self, 'updateToggle', [togg, true])

		else:
			var e = control[1]
			e.set_pos(Vector2(10, y))
			e.set_theme(theme)
			if e extends Button:
				e.set_text(control[0])
			if control[0] == 'save_pzl' :
				e.connect('pressed', self, 'puzzleSave')
			if control[0] == 'load_pzl' :
				e.connect('pressed', self, 'puzzleSave', [true])
			if control[0] == 'status':
				control[1].set_text('STATUS: NOMINAL')
			add_child(e)


saner initialization

saner initialization

extends Spatial

# proposed script manager:
# var PuzzleManScript = get_node("Globals").get("PuzzleManScript")

var DataMan = preload( "res://scripts/DataManager.gd" ).new()
var PuzzleManScript = preload( "res://scripts/PuzzleManager.gd" )
var PuzzleScn = preload("res://puzzle.scn")
var puzzleMan
var otherPuzzle
var mainPuzzle = true

var time = {
		on = true,
		val = 0.0,
		label = Label.new(),
		tween = Tween.new() }

func addTimer():
	add_child(time.label)
	add_child(time.tween)
	time.label.set_pos(Vector2(15,15))

	time.label.set_theme(load("res://themes/MainTheme.thm"))
	time.tween.interpolate_method(time.label, "set_pos", \
			time.label.get_pos(), time.label.get_pos()*2, 0.5, \
			Tween.TRANS_BOUNCE, Tween.EASE_OUT)
	time.tween.start()
	time.label.set_text(str(time.val))

func formatTime(t):
	var mins = floor(t / 60)
	var secs = fmod(floor(t), 60)
	var millis = floor((t - floor(t)) * 1000)
	return str(mins) + ":" + str(secs).pad_zeros(2) + ":" + str(millis).pad_zeros(3)

func _process(dTime):
	# start label tween on
	time.val += dTime
	time.label.set_text(formatTime(time.val))
	if fmod(time.val, 10) < 0.1:
		time.tween.seek(0.0)

# Called for initialization
func _ready():
	if time.on:
		set_process(true) # needed for time keeping
	puzzleMan = PuzzleManScript.new()
	var puzzle = puzzleMan.generatePuzzle( 2, puzzleMan.DIFF_EASY )
	puzzle.puzzleMan = puzzleMan

	print("Generated ", puzzle.blocks.size(), " blocks." )

	print( "Saving..." )
	DataMan.savePuzzle( "TestPuzzle.pzl", puzzle )

	puzzle = 0

	puzzle = DataMan.loadPuzzle( "TestPuzzle.pzl" )

	var steps = puzzle.solvePuzzleSteps()
	print( "PUZZLE IS SOLVEABLE?: ", steps.solveable )

	addTimer()

	# Place the blocks in the puzzle.
	var gridMan = get_node( "GridView/GridMan" )
	gridMan.shape = puzzleMan.shape
	gridMan.set_puzzle(puzzle)
	for block in puzzle.blocks:
		# Create a block node, add it to the tree
		gridMan.add_block(block.toNode())

	# make a new puzzle, embed using Viewport
	if mainPuzzle:
		var p = PuzzleScn.instance()
		p.remove_and_delete_child(p.get_node("Lights"))
		p.get_node("GridView").active = false
		p.mainPuzzle = false
		p.set_scale(Vector3(0.5, 0.5, 0.5))
		p.set_translation(Vector3(10, 5, -20))

		var v = Viewport.new()
		var c = Control.new()

		v.set_world(p.get_world())
		v.set_rect(Rect2(0, 0, 100, 100))
		v.set_physics_object_picking(false)
		get_node("Camera").add_child(p)
		v.add_child(p)
		add_child(c)
		c.add_child(v)
		otherPuzzle = p #for use with network
# child of control? easier input

extends Spatial

# proposed script manager:
# var PuzzleManScript = get_node("Globals").get("PuzzleManScript")

var DataMan = preload( "res://scripts/DataManager.gd" ).new()
var PuzzleManScript = preload( "res://scripts/PuzzleManager.gd" )
var PuzzleScn = preload("res://puzzle.scn")
var puzzleMan
var otherPuzzle
var mainPuzzle = true

var time = {
		on = true,
		val = 0.0,
		label = null,
		tween = null }

func addTimer():
	time.label = Label.new()
	time.tween = Tween.new()
	add_child(time.label)
	add_child(time.tween)
	time.label.set_pos(Vector2(15,15))

	time.label.set_theme(load("res://themes/MainTheme.thm"))
	time.tween.interpolate_method(time.label, "set_pos", \
			time.label.get_pos(), time.label.get_pos()*2, 0.5, \
			Tween.TRANS_BOUNCE, Tween.EASE_OUT)
	time.tween.start()
	time.label.set_text(str(time.val))

func formatTime(t):
	var mins = floor(t / 60)
	var secs = fmod(floor(t), 60)
	var millis = floor((t - floor(t)) * 1000)
	return str(mins) + ":" + str(secs).pad_zeros(2) + ":" + str(millis).pad_zeros(3)

func _process(dTime):
	# start label tween on
	time.val += dTime
	time.label.set_text(formatTime(time.val))
	if fmod(time.val, 10) < 0.1:
		time.tween.seek(0.0)

# Called for initialization
func _ready():
	if time.on:
		set_process(true) # needed for time keeping
	puzzleMan = PuzzleManScript.new()
	var puzzle = puzzleMan.generatePuzzle( 2, puzzleMan.DIFF_EASY )
	puzzle.puzzleMan = puzzleMan

	print("Generated ", puzzle.blocks.size(), " blocks." )

	print( "Saving..." )
	DataMan.savePuzzle( "TestPuzzle.pzl", puzzle )

	puzzle = 0

	puzzle = DataMan.loadPuzzle( "TestPuzzle.pzl" )

	var steps = puzzle.solvePuzzleSteps()
	print( "PUZZLE IS SOLVEABLE?: ", steps.solveable )

	addTimer()

	# Place the blocks in the puzzle.
	var gridMan = get_node( "GridView/GridMan" )
	gridMan.shape = puzzleMan.shape
	gridMan.set_puzzle(puzzle)
	for block in puzzle.blocks:
		# Create a block node, add it to the tree
		gridMan.add_block(block.toNode())

	# make a new puzzle, embed using Viewport
	if mainPuzzle:
		var p = PuzzleScn.instance()
		p.remove_and_delete_child(p.get_node("Lights"))
		p.get_node("GridView").active = false
		p.mainPuzzle = false
		p.set_scale(Vector3(0.5, 0.5, 0.5))
		p.set_translation(Vector3(10, 5, -20))

		var v = Viewport.new()
		var c = Control.new()

		v.set_world(p.get_world())
		v.set_rect(Rect2(0, 0, 100, 100))
		v.set_physics_object_picking(false)
		get_node("Camera").add_child(p)
		v.add_child(p)
		add_child(c)
		c.add_child(v)
		otherPuzzle = p #for use with network
# child of control? easier input


Cleaned

Cleaned

extends Node2D
var mainArray = []
const BALL_SIZE = 64
var ballObj = load("res://objects/ball.tscn")
var slot = 0
var ballPressed = false
var ballPressedName = ''
var shiftPressed = Vector2(0,0)

func _fixed_process(delta):
	var mouse = get_viewport().get_mouse_pos()
	var ss = str(mouse)
	var ballPressedPos = findBallByName(ballPressedName)

	if ballPressed:
		ss += " __" + str(ballPressedPos)
		var mouseOnGrid = Vector2(0,0)
		var onGrid = Vector2(0,0)

		mouseOnGrid.x = int((mouse.x - ballPressedPos.x) / 64) - 1
		mouseOnGrid.y = int((mouse.y - 32 - ballPressedPos.y) / 64)
		ss += " **" + str(mouseOnGrid)

		onGrid.x = (int((mouse.x - ballPressedPos.x) / 64) - 1) - ballPressedPos.x
		onGrid.y = int((mouse.y - 32 - ballPressedPos.y) / 64) - ballPressedPos.y
		ss += " " + str(onGrid)

		if onGrid.x != 0:
			var ballsToShift = []
			if ballPressedPos.y > 6:
				ballsToShift.append(get_node("balls/b" + ballPressedName))

			else:
				for i in range(6):
					var b = get_node("balls/b" + mainArray[i * 7 + ballPressedPos.y])
					ballsToShift.append(b)
			for i in range(ballsToShift.size()):

				var b = get_node("balls/b" + mainArray[mainArray.size() - 1])
				if ballPressedPos.y < 7:
					b = get_node("balls/b" + mainArray[i * 7 + ballPressedPos.y])
				var pos = b.get_pos()
				pos.x = i * 64 + mouseOnGrid.x * 64 - ballPressedPos.x * 64
				if ballPressedPos.y > 6:
					pos.x = i * 64 + mouseOnGrid.x * 64
				if pos.x > 64 * 6 - 32:
					pos.x -= 64 * 6
				elif pos.x < 0:
					pos.x += 64 * 6
				b.set_pos(pos)
			shiftPressed = Vector2(ballPressedPos.y, onGrid.x)

	get_node("txt").set_text(ss)
	pass

func _ready():
	randomize()
	set_process_input(true)
	set_fixed_process(true)
#Ключовий масив, в котрому зберігаються дані про положення кольорових кульок
	mainArray = [ 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7',
				 'o1', 'o2', 'o3', 'o4', 'o5', 'o6', 'o7',
				 'y1', 'y2', 'y3', 'y4', 'y5', 'y6', 'y7',
				 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g7',
				 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7',
				 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7',
				 '']
#Створення кульок відповідно до ключового масиву
	shuffleBalls()
	updateBalls()

#Обробка подій (натискання клавіш)
func _input(event):
	if event.is_action_released("space"):
		shuffleBalls()
		updateBalls()
	if event.is_action_released("LMB"):
		ballPressed = false

		if shiftPressed.y != 0:
			shiftRow(shiftPressed.x, shiftPressed.y)
		else:
			if findBallByName('').x == findBallByName(ballPressedName).x:

				var sCol = findBallByName(ballPressedName).x
				cutCol(findBallByName(ballPressedName))
				updateBalls()
		updateBalls()
		shiftPressed = Vector2(0,0)

#Створення однієї кульки за заданими параметрами
func createBall(name):
	var balls = get_node("balls")
	var ball = ballObj.instance()
	ball.set_name(name)
	balls.add_child(ball)
	var color = Color(0,0,0,1)
	if name != 'b':
		color = toColor(name[1])
	ball.setColor(color)

#Перефарбування кульок у відповідність до зазначених в ключовому масиві кольорів
func toColor(colorMark):
	var color = Color(1,1,1,1)
	if not colorMark: 			#Порожня комірка
		color = Color(0,0,0,1)
	elif colorMark == 'r':		#Червоні
		color = Color(1,0,0,1)
	elif colorMark == 'g':		#Зелені
		color = Color(0,0.6,0,1)
	elif colorMark == 'b':		#Сині
		color = Color(0,0,1,1)
	elif colorMark == 'o':		#Помаранчеві
		color = Color(1.0, 0.5, 0.0, 1.0)
	elif colorMark == 'y':		#Жовті
		color = Color(0.65, 0.65, 0.0, 1.0)
	elif colorMark == 'p':		#Фіолетові
		color = Color(0.65, 0.0, 0.65, 1.0)
	return color

#Зміщення рядка ліворуч або праворуч
func shiftRow(row, dir):
	if abs(row) == 7:
		slot += dir
		if slot < 0:
			slot = 5
		elif slot > 5:
			slot = 0
		return 0
	var tempRow = []
	for i in range(6):
		tempRow.append(mainArray[i * 7 + row])
	var tail
	if dir < 0:
		for j in range(abs(dir)):
			tail = tempRow[0]
			for i in range(tempRow.size() - 1):
				tempRow[i] = tempRow[i+1]
			tempRow[tempRow.size() - 1] = tail
	elif dir > 0:
		for j in range(abs(dir)):
			tail = tempRow[tempRow.size() - 1]
			for i in range((tempRow.size() - 1), 0 , -1):
				tempRow[i] = tempRow[i - 1]
			tempRow[0] = tail
	for i in range(6):
		mainArray[i * 7 + row] = tempRow[i]

func findBallByName(name):
	var index = 0
	for b in mainArray:
		if b == name:
			var col = int(index / 7)
			var row = (index - col * 7)
			if index == mainArray.size() - 1:
				return(Vector2(slot,7))
			return(Vector2(col,row))
		index += 1

#Перемішати кульки
func shuffleBalls():
	var shuffledList = []
	var indexList = range(mainArray.size())
	for i in range(mainArray.size()):
		var x = randi()%indexList.size()
		shuffledList.append(mainArray[indexList[x]])
		indexList.remove(x)
	mainArray = shuffledList
	slot = randi()%6

#Обновлення позицій кульок
func updateBalls():
	var index = 0
	var table = []
	for b in mainArray:
		var col = int(index / 7)
		var row = (index - col * 7)
		var name = "b" + mainArray[index]
		if not get_node("balls/" + name):
			createBall(name)
		if index < mainArray.size() - 1:
			get_node("balls" + "/b" + mainArray[index]).set_pos(Vector2(col * (BALL_SIZE), row * (BALL_SIZE)))
		else:
			get_node("balls" + "/b" + mainArray[index]).set_pos(Vector2(slot * BALL_SIZE, 7 * BALL_SIZE))
		index += 1

#Обробка сигналів кнопок
func _signal_arrow(rowDir):
		shiftRow(abs(rowDir) - 1, sign(int(rowDir)))
		updateBalls()

func cutCol(ball):
	var column = []
	var i
	for b in range(7):
		i = b + (ball.x * 7)
		column.append(mainArray[i])
	if slot == ball.x:
		column.append(mainArray[mainArray.size() - 1])
	var empty = findBallByName('')

	var dir = 1
	if empty.y > ball.y:
		dir = -1
	i = empty.y
	while(i != ball.y):
		column[i] = column[i + dir]

		i += dir
	column[ball.y] = ''

	for b in range(7):
		i = b + (ball.x * 7)
		mainArray[i] = column[b]
	if slot == ball.x:
		mainArray[mainArray.size() - 1] = column[column.size() - 1]

func _signal_ballClicked(name):
	if name != 'b':
		name = name[1] + name[2]
		ballPressedName = name
	else:
		ballPressedName = ''
	ballPressed = true

extends Node2D
var mainArray = []
const BALL_SIZE = 64
var ballObj = load("res://objects/ball.tscn")
var slot = 0
var ballPressed = false
var ballPressedName = ''
var shiftPressed = Vector2(0,0)
#var ballPressedPos = Vector2(0,0)

func _fixed_process(delta):
	var mouse = get_viewport().get_mouse_pos()
	var ss = str(mouse)
	var ballPressedPos = findBallByName(ballPressedName)

	if ballPressed:
		ss += " __" + str(ballPressedPos)
		var mouseOnGrid = Vector2(0,0)
		var onGrid = Vector2(0,0)

		mouseOnGrid.x = int((mouse.x - ballPressedPos.x) / 64) - 1
		mouseOnGrid.y = int((mouse.y - 32 - ballPressedPos.y) / 64)
		ss += " **" + str(mouseOnGrid)

		onGrid.x = (int((mouse.x - ballPressedPos.x) / 64) - 1) - ballPressedPos.x
		onGrid.y = int((mouse.y - 32 - ballPressedPos.y) / 64) - ballPressedPos.y
		ss += " " + str(onGrid)

		if onGrid.x != 0:
			var ballsToShift = []
			if ballPressedPos.y > 6:
				ballsToShift.append(get_node("balls/b" + ballPressedName))
				print(ballPressedName)
			else:
				for i in range(6):
					var b = get_node("balls/b" + mainArray[i * 7 + ballPressedPos.y])
					ballsToShift.append(b)
			for i in range(ballsToShift.size()):
				print(i)
				var b = get_node("balls/b" + mainArray[mainArray.size() - 1])
				if ballPressedPos.y < 7:
					b = get_node("balls/b" + mainArray[i * 7 + ballPressedPos.y])
				var pos = b.get_pos()
				pos.x = i * 64 + mouseOnGrid.x * 64 - ballPressedPos.x * 64
				if ballPressedPos.y > 6:
					pos.x = i * 64 + mouseOnGrid.x * 64
				print(ballPressedPos)
				if pos.x > 64 * 6 - 32:
					pos.x -= 64 * 6
				elif pos.x < 0:
					pos.x += 64 * 6
				b.set_pos(pos)
			shiftPressed = Vector2(ballPressedPos.y, onGrid.x)

	get_node("txt").set_text(ss)
	pass

func _ready():
	randomize()
	set_process_input(true)
	set_fixed_process(true)
#Ключовий масив, в котрому зберігаються дані про положення кольорових кульок
	mainArray = [ 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7',
				 'o1', 'o2', 'o3', 'o4', 'o5', 'o6', 'o7',
				 'y1', 'y2', 'y3', 'y4', 'y5', 'y6', 'y7',
				 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g7',
				 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7',
				 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7',
				 '']
#Створення кульок відповідно до ключового масиву
	shuffleBalls()
	updateBalls()

#Обробка подій (натискання клавіш)
func _input(event):
	if event.is_action_released("space"):
		shuffleBalls()
		updateBalls()
	if event.is_action_released("LMB"):
		ballPressed = false

		if shiftPressed.y != 0:
			shiftRow(shiftPressed.x, shiftPressed.y)
		else:
			if findBallByName('').x == findBallByName(ballPressedName).x:
				#print(findBallByName(name))
				var sCol = findBallByName(ballPressedName).x
				cutCol(findBallByName(ballPressedName))
				updateBalls()
		updateBalls()
		shiftPressed = Vector2(0,0)

#Створення однієї кульки за заданими параметрами
func createBall(name):
	var balls = get_node("balls")
	var ball = ballObj.instance()
	ball.set_name(name)
	balls.add_child(ball)
	var color = Color(0,0,0,1)
	if name != 'b':
		color = toColor(name[1])
	ball.setColor(color)

#Перефарбування кульок у відповідність до зазначених в ключовому масиві кольорів
func toColor(colorMark):
	var color = Color(1,1,1,1)
	if not colorMark: 			#Порожня комірка
		color = Color(0,0,0,1)
	elif colorMark == 'r':		#Червоні
		color = Color(1,0,0,1)
	elif colorMark == 'g':		#Зелені
		color = Color(0,0.6,0,1)
	elif colorMark == 'b':		#Сині
		color = Color(0,0,1,1)
	elif colorMark == 'o':		#Помаранчеві
		color = Color(1.0, 0.5, 0.0, 1.0)
	elif colorMark == 'y':		#Жовті
		color = Color(0.65, 0.65, 0.0, 1.0)
	elif colorMark == 'p':		#Фіолетові
		color = Color(0.65, 0.0, 0.65, 1.0)
	return color

#Зміщення рядка ліворуч або праворуч
func shiftRow(row, dir):
	if abs(row) == 7:
		slot += dir
		if slot < 0:
			slot = 5
		elif slot > 5:
			slot = 0
		return 0
	var tempRow = []
	for i in range(6):
		tempRow.append(mainArray[i * 7 + row])
	var tail
	if dir < 0:
		for j in range(abs(dir)):
			tail = tempRow[0]
			for i in range(tempRow.size() - 1):
				tempRow[i] = tempRow[i+1]
			tempRow[tempRow.size() - 1] = tail
	elif dir > 0:
		for j in range(abs(dir)):
			tail = tempRow[tempRow.size() - 1]
			for i in range((tempRow.size() - 1), 0 , -1):
				tempRow[i] = tempRow[i - 1]
			tempRow[0] = tail
	for i in range(6):
		mainArray[i * 7 + row] = tempRow[i]

func findBallByName(name):
	var index = 0
	for b in mainArray:
		if b == name:
			var col = int(index / 7)
			var row = (index - col * 7)
			if index == mainArray.size() - 1:
				return(Vector2(slot,7))
			return(Vector2(col,row))
		index += 1

#Перемішати кульки
func shuffleBalls():
	var shuffledList = []
	var indexList = range(mainArray.size())
	for i in range(mainArray.size()):
		var x = randi()%indexList.size()
		shuffledList.append(mainArray[indexList[x]])
		indexList.remove(x)
	mainArray = shuffledList
	slot = randi()%6

#Обновлення позицій кульок
func updateBalls():
	var index = 0
	var table = []
	for b in mainArray:
		var col = int(index / 7)
		var row = (index - col * 7)
		var name = "b" + mainArray[index]
		if not get_node("balls/" + name):
			createBall(name)
		if index < mainArray.size() - 1:
			get_node("balls" + "/b" + mainArray[index]).set_pos(Vector2(col * (BALL_SIZE), row * (BALL_SIZE)))
		else:
			get_node("balls" + "/b" + mainArray[index]).set_pos(Vector2(slot * BALL_SIZE, 7 * BALL_SIZE))
		index += 1

#Обробка сигналів кнопок
func _signal_arrow(rowDir):
		shiftRow(abs(rowDir) - 1, sign(int(rowDir)))
		updateBalls()

func cutCol(ball):
	var column = []
	var i
	for b in range(7):
		i = b + (ball.x * 7)
		column.append(mainArray[i])
	if slot == ball.x:
		column.append(mainArray[mainArray.size() - 1])
	var empty = findBallByName('')

	var dir = 1
	if empty.y > ball.y:
		dir = -1
	i = empty.y
	while(i != ball.y):
		column[i] = column[i + dir]

		i += dir
	column[ball.y] = ''

	for b in range(7):
		i = b + (ball.x * 7)
		mainArray[i] = column[b]
	if slot == ball.x:
		mainArray[mainArray.size() - 1] = column[column.size() - 1]

func _signal_ballClicked(name):
	if name != 'b':
		name = name[1] + name[2]
		ballPressedName = name
	else:
		ballPressedName = ''
	ballPressed = true

#		if findBallByName('').x == findBallByName(name).x:
#			print(findBallByName(name))
#			var sCol = findBallByName(name).x
#			cutCol(findBallByName(name))
#			updateBalls()


clearer name BLOCK_TRANSFORM -> PUZZLE_TRANSFORM. puzzle is sent over network

# Core networking

extends Node

const REMOTE_FINISH = 0
const REMOTE_START = 1
const REMOTE_QUIT = 2
const REMOTE_BLOCK = 4
const REMOTE_PUZZLE_TRANSFORM = 5
const REMOTE_BLOCK_UPDATE = 6

var port = 54321
var root
var isNetwork
var isHost
var isClient = false
var server
var client
var connection
var proxy

var thisPuzzle

#Store the peer stream used by both the lcient and server
var stream

func _ready():
	isNetwork = false
	isHost = false
	var label = get_tree().get_root().get_node("GUIManager/OptionsMenu/Panel/PortField/LineEdit")
	if not label == null:
		label.set_text(str(port))

func setPort(pt):
	port = pt;

func connectTo(ip, pt):
	isClient = true #just to tell if it's doing something :S
	connection = PacketPeerStream.new()
	stream = StreamPeerTCP.new()
	connection.set_stream_peer(stream)
	stream.connect(ip, pt);

	if stream.get_status() == stream.STATUS_CONNECTED or stream.get_status() == stream.STATUS_CONNECTING:
		print("Connecting to " + ip + ":" + str(port));
		proxy.set_process(true)
		#leave is_network as false to indicate we're still waiting for connection

func host(pt):
	server = TCP_Server.new()
	#connection = PacketPeerStream.new()
	isHost = true
	print("Starting listening server on port " + str(pt))
	if server.listen(pt) == 0:
		proxy.set_process(true)
		print("Listening...")
	else:
		print("Failed to start server on port " + str(pt));

func _process(delta):
	if (isHost):
		#server processing stuff
		#use isNetwork to determine if we're still listening
		if !isNetwork:
			if server.is_connection_available():
				stream = server.take_connection()
				connection = PacketPeerStream.new()
				connection.set_stream_peer(stream)
				print("Connecting with player...")
				isNetwork = true
				server.stop()

				gotoPuzzle()

				thisPuzzle = root.get_node("Puzzle")
				#MAKE CALL TO PUZZLE SELECTER
		else: #not listening anymore, have a client
			#do quick check to make sure we're still
			#connected
			if !stream.is_connected():
				print("Lost Connection!");
				isNetwork = false
				proxy.set_process(false)
				#QUIT GAME
				return
			#check if we have any data
			if connection.get_available_packet_count() > 0:
				#have to be careful about more than 1 packet per frame
				for i in range(connection.get_available_packet_count()):
					var dataArray = connection.get_var()
					ProcessServerData(dataArray)

	else:
		#client processing stuff
		if !isNetwork:
			#Still waiting for connection confirmed
			if stream.get_status() == stream.STATUS_CONNECTED:
				print("Connection established!")
				isNetwork = true

				gotoPuzzle()

				thisPuzzle = root.get_node("Puzzle")
				return
			if stream.get_status() == stream.STATUS_NONE or stream.get_status() == stream.STATUS_ERROR:
				print("Error establishing connection!")
				proxy.set_process(false)
				return
				#stop running process loop, cause we have no connection
		else:
			#connecton established and confirmed. Do regular data processing
			#check if we have any data
			if connection.get_available_packet_count() > 0:
				for i in range(connection.get_available_packet_count()):
					var dataArray = connection.get_var()
					ProcessServerData(dataArray)
					#Call the server process script cuase it's peer to peer and we have the same functions!

func disconnect():
	#need to do lots of things! If the server is open and listening, but has no connection just stop listening!
	if isHost and !isNetwork:
		#this means we're waiting for connection still
		server.stop()
		print("closing server listener!")
	elif isHost and isNetwork:
		#have connections. Need to send them stop packet, and close connection
		connection.put_var([REMOTE_QUIT])
		stream.disconnect()
		print("Closing connection to remote player...")
	elif !isHost and !isNetwork:
		stream.disconnect()
		print("Halting connection request...")
	elif !isHost and isNetwork:
		#connected to server already
		stream.disconnect()
		print("Disconnecting from server...")

	#no matter where we wre before disconnect, set network to false and return to beginning screen!
	isNetwork = false;
	isHost = false;
	isClient = false;

	proxy.set_process(false)

	gotoMenu()

func ProcessServerData(dataArray):
	#have an array of data. First element should be identifying int
	var ID = dataArray[0]
	var puzzle = root.get_node( "Puzzle" )
	var gridMan = puzzle.otherPuzzle.get_node("GridView/GridMan")
	#no switch statement. I'm crying right now while i type this. My fingers are bleeding
	if ID == REMOTE_START:
		#read other person's puzzle
		print("remote_stuff")
		gridMan.set_puzzle(dataArray[1])
	elif ID == REMOTE_FINISH:
		#again, do ending stuff
		print("remote_finish: " + str(dataArray[1]))
		gridMan.beatenWithScore(dataArray[1])
	elif ID == REMOTE_QUIT:
		#omg those jerks!
		print("remote_quit")
		disconnect()
	elif ID == REMOTE_BLOCK:
		#get their block ifnormation!
		print("remote_block")
	elif ID == REMOTE_PUZZLE_TRANSFORM:
		#sent block information
		var translation = dataArray[1]
		thisPuzzle.otherPuzzle.set_transform(Transform( translation ))
# better measurements!!!!		thisPuzzle.otherPuzzle.set_translation(Vector3(20, 12, -40))
##############3
		thisPuzzle.otherPuzzle.set_translation(Vector3(10, 5, -20))
	elif ID == REMOTE_BLOCK_UPDATE:
		#sent an updated block pair
		print("block update")
		var pos = dataArray[1]
		gridMan.forceClickBlock(pos)

func sendStart(puzzle):
	if !isNetwork:
		print("Error sending start packet: not connected!")
		return
	var er = connection.put_var([REMOTE_START, puzzle])

func sendBlockUpdate(pos):
	if !isNetwork:
		print("Error sending start packet: not connected!")
		return
	connection.put_var([REMOTE_BLOCK_UPDATE, pos])

func sendFinish(score):
	if !isNetwork:
		print("Error sending finish packet: not connected!")
		return
	connection.put_var([REMOTE_FINISH, root.get_node("Puzzle").time.val])

func sendQuit():
	if !isNetwork:
		print("Error sending finish packet: not connected!")
		return
	connection.put_var([REMOTE_QUIT])

func sendTransform(translation):
	if !isNetwork:
		print("Cannot send transformation over an unitialized network!")
		return
	connection.put_var([REMOTE_PUZZLE_TRANSFORM, translation])
	print("it got here")

func gotoMenu():
	root.get_child( root.get_child_count() - 1 ).queue_free()
	root.add_child( load("res://menus.scn") )

func gotoPuzzle():
	var guiMan = preload("GUIManager.gd").new()
	guiMan.gotoPuzzleScene(root, true) # network = true

# Core networking

extends Node

const REMOTE_FINISH = 0
const REMOTE_START = 1
const REMOTE_QUIT = 2
const REMOTE_BLOCK = 4
const REMOTE_BLOCK_TRANSFORM = 5
const REMOTE_BLOCK_UPDATE = 6

var port = 54321
var root
var isNetwork
var isHost
var isClient = false
var server
var client
var connection
var proxy

var remotePuzzle

#Store the peer stream used by both the lcient and server
var stream

func _ready():
	isNetwork = false
	isHost = false
	var label = get_tree().get_root().get_node("GUIManager/OptionsMenu/Panel/PortField/LineEdit")
	if not label == null:
		label.set_text(str(port))

func setPort(pt):
	port = pt;

func connectTo(ip, pt):
	isClient = true #just to tell if it's doing something :S
	connection = PacketPeerStream.new()
	stream = StreamPeerTCP.new()
	connection.set_stream_peer(stream)
	stream.connect(ip, pt);

	if stream.get_status() == stream.STATUS_CONNECTED or stream.get_status() == stream.STATUS_CONNECTING:
		print("Connecting to " + ip + ":" + str(port));
		proxy.set_process(true)
		#leave is_network as false to indicate we're still waiting for connection

func host(pt):
	server = TCP_Server.new()
	#connection = PacketPeerStream.new()
	isHost = true
	print("Starting listening server on port " + str(pt))
	if server.listen(pt) == 0:
		proxy.set_process(true)
		print("Listening...")
	else:
		print("Failed to start server on port " + str(pt));

func _process(delta):
	if (isHost):
		#server processing stuff
		#use isNetwork to determine if we're still listening
		if !isNetwork:
			if server.is_connection_available():
				stream = server.take_connection()
				connection = PacketPeerStream.new()
				connection.set_stream_peer(stream)
				print("Connecting with player...")
				isNetwork = true
				server.stop()

				gotoPuzzle()

				remotePuzzle = root.get_node("Puzzle")
				#MAKE CALL TO PUZZLE SELECTER
		else: #not listening anymore, have a client
			#do quick check to make sure we're still
			#connected
			if !stream.is_connected():
				print("Lost Connection!");
				isNetwork = false
				proxy.set_process(false)
				#QUIT GAME
				return
			#check if we have any data
			if connection.get_available_packet_count() > 0:
				#have to be careful about more than 1 packet per frame
				for i in range(connection.get_available_packet_count()):
					var dataArray = connection.get_var()
					ProcessServerData(dataArray)

	else:
		#client processing stuff
		if !isNetwork:
			#Still waiting for connection confirmed
			if stream.get_status() == stream.STATUS_CONNECTED:
				print("Connection established!")
				isNetwork = true

				gotoPuzzle()

				remotePuzzle = root.get_node("Puzzle")
				return
			if stream.get_status() == stream.STATUS_NONE or stream.get_status() == stream.STATUS_ERROR:
				print("Error establishing connection!")
				proxy.set_process(false)
				return
				#stop running process loop, cause we have no connection
		else:
			#connecton established and confirmed. Do regular data processing
			#check if we have any data
			if connection.get_available_packet_count() > 0:
				for i in range(connection.get_available_packet_count()):
					var dataArray = connection.get_var()
					ProcessServerData(dataArray)
					#Call the server process script cuase it's peer to peer and we have the same functions!

func disconnect():
	#need to do lots of things! If the server is open and listening, but has no connection just stop listening!
	if isHost and !isNetwork:
		#this means we're waiting for connection still
		server.stop()
		print("closing server listener!")
	elif isHost and isNetwork:
		#have connections. Need to send them stop packet, and close connection
		connection.put_var([REMOTE_QUIT])
		stream.disconnect()
		print("Closing connection to remote player...")
	elif !isHost and !isNetwork:
		stream.disconnect()
		print("Halting connection request...")
	elif !isHost and isNetwork:
		#connected to server already
		stream.disconnect()
		print("Disconnecting from server...")

	#no matter where we wre before disconnect, set network to false and return to beginning screen!
	isNetwork = false;
	isHost = false;
	isClient = false;

	proxy.set_process(false)

	gotoMenu()

func ProcessServerData(dataArray):
	#have an array of data. First element should be identifying int
	var ID = dataArray[0]
	print(ID)
	#no switch statement. I'm crying right now while i type this. My fingers are bleeding
	if ID == REMOTE_START:
		#do start something or something whut
		print("remote_stuff")
	elif ID == REMOTE_FINISH:
		#again, do ending stuff
		print("remote_finish: " + str(dataArray[1]))
	elif ID == REMOTE_QUIT:
		#omg those jerks!
		print("remote_quit")
		disconnect()
	elif ID == REMOTE_BLOCK:
		#get their block ifnormation!
		print("remote_block")
	elif ID == REMOTE_BLOCK_TRANSFORM:
		#sent block information
		print("block TRANSFORM!!!!!!!!!!!")
		#var scale = dataArray[1]
		var translation = dataArray[1]
		#remotePuzzle.otherPuzzle.set_scale(scale)
		remotePuzzle.otherPuzzle.set_transform(Transform( translation ))
		remotePuzzle.otherPuzzle.set_translation(Vector3(20, 12, -40))
	elif ID == REMOTE_BLOCK_UPDATE:
		#sent an updated block pair
		print("block update")
		var puzzle = root.get_node( "Puzzle" )
		var pos = dataArray[1]
		puzzle.otherPuzzle.get_node("GridView/GridMan").forceClickBlock(pos)

func sendStart():
	if !isNetwork:
		print("Error sending start packet: not connected!")
		return
	var er = connection.put_var([REMOTE_START])
	print("Sending start and got [" + str(er) + "]")

func sendBlockUpdate(pos):
	if !isNetwork:
		print("Error sending start packet: not connected!")
		return
	connection.put_var([REMOTE_BLOCK_UPDATE, pos])

func sendFinish(score):
	if !isNetwork:
		print("Error sending finish packet: not connected!")
		return
	connection.put_var([REMOTE_FINISH, score])

func sendQuit():
	if !isNetwork:
		print("Error sending finish packet: not connected!")
		return
	connection.put_var([REMOTE_QUIT])

func sendTransform(translation):
	if !isNetwork:
		print("Cannot send transformation over an unitialized network!")
		return
	connection.put_var([REMOTE_BLOCK_TRANSFORM, translation])
	print("it got here")

func gotoMenu():
	root.get_child( root.get_child_count() - 1 ).queue_free()
	root.add_child( load("res://menus.scn") )

func gotoPuzzle():
	var guiMan = preload("GUIManager.gd").new()
	guiMan.gotoPuzzleScene(root, true) # network = true


Don't check bullet collision with other bullets in the Bullet shower demo

This improves performance by reducing the number of collision checks
that need to be done.

extends Node2D
# This demo is an example of controling a high number of 2D objects with logic
# and collision without using nodes in the scene. This technique is a lot more
# efficient than using instancing and nodes, but requires more programming and
# is less visual. Bullets are managed together in the `bullets.gd` script.

const BULLET_COUNT = 500
const SPEED_MIN = 20
const SPEED_MAX = 80

const bullet_image = preload("res://bullet.png")

var bullets = []
var shape

class Bullet:
	var position = Vector2()
	var speed = 1.0
	# The body is stored as a RID, which is an "opaque" way to access resources.
	# With large amounts of objects (thousands or more), it can be significantly
	# faster to use RIDs compared to a high-level approach.
	var body = RID()

func _ready():
	randomize()

	shape = Physics2DServer.circle_shape_create()
	# Set the collision shape's radius for each bullet in pixels.
	Physics2DServer.shape_set_data(shape, 8)

	for _i in BULLET_COUNT:
		var bullet = Bullet.new()
		# Give each bullet its own speed.
		bullet.speed = rand_range(SPEED_MIN, SPEED_MAX)
		bullet.body = Physics2DServer.body_create()

		Physics2DServer.body_set_space(bullet.body, get_world_2d().get_space())
		Physics2DServer.body_add_shape(bullet.body, shape)
		# Don't make bullets check collision with other bullets to improve performance.
		# Their collision mask is still configured to the default value, which allows
		# bullets to detect collisions with the player.
		Physics2DServer.body_set_collision_layer(bullet.body, 0)

		# Place bullets randomly on the viewport and move bullets outside the
		# play area so that they fade in nicely.
		bullet.position = Vector2(
			rand_range(0, get_viewport_rect().size.x) + get_viewport_rect().size.x,
			rand_range(0, get_viewport_rect().size.y)
		)
		var transform2d = Transform2D()
		transform2d.origin = bullet.position
		Physics2DServer.body_set_state(bullet.body, Physics2DServer.BODY_STATE_TRANSFORM, transform2d)

		bullets.push_back(bullet)

func _process(_delta):
	# Order the CanvasItem to update every frame.
	update()

func _physics_process(delta):
	var transform2d = Transform2D()
	var offset = get_viewport_rect().size.x + 16
	for bullet in bullets:
		bullet.position.x -= bullet.speed * delta

		if bullet.position.x < -16:
			# The bullet has left the screen; move it back to the right.
			bullet.position.x = offset

		transform2d.origin = bullet.position

		Physics2DServer.body_set_state(bullet.body, Physics2DServer.BODY_STATE_TRANSFORM, transform2d)

# Instead of drawing each bullet individually in a script attached to each bullet,
# we are drawing *all* the bullets at once here.
func _draw():
	var offset = -bullet_image.get_size() * 0.5
	for bullet in bullets:
		draw_texture(bullet_image, bullet.position + offset)

# Perform cleanup operations (required to exit without error messages in the console).
func _exit_tree():
	for bullet in bullets:
		Physics2DServer.free_rid(bullet.body)

	Physics2DServer.free_rid(shape)
	bullets.clear()

extends Node2D
# This demo is an example of controling a high number of 2D objects with logic
# and collision without using nodes in the scene. This technique is a lot more
# efficient than using instancing and nodes, but requires more programming and
# is less visual. Bullets are managed together in the `bullets.gd` script.

const BULLET_COUNT = 500
const SPEED_MIN = 20
const SPEED_MAX = 80

const bullet_image = preload("res://bullet.png")

var bullets = []
var shape

class Bullet:
	var position = Vector2()
	var speed = 1.0
	# The body is stored as a RID, which is an "opaque" way to access resources.
	# With large amounts of objects (thousands or more), it can be significantly
	# faster to use RIDs compared to a high-level approach.
	var body = RID()

func _ready():
	randomize()

	shape = Physics2DServer.circle_shape_create()
	# Set the collision shape's radius for each bullet in pixels.
	Physics2DServer.shape_set_data(shape, 8)

	for _i in BULLET_COUNT:
		var bullet = Bullet.new()
		# Give each bullet its own speed.
		bullet.speed = rand_range(SPEED_MIN, SPEED_MAX)
		bullet.body = Physics2DServer.body_create()

		Physics2DServer.body_set_space(bullet.body, get_world_2d().get_space())
		Physics2DServer.body_add_shape(bullet.body, shape)

		# Place bullets randomly on the viewport and move bullets outside the
		# play area so that they fade in nicely.
		bullet.position = Vector2(
			rand_range(0, get_viewport_rect().size.x) + get_viewport_rect().size.x,
			rand_range(0, get_viewport_rect().size.y)
		)
		var transform2d = Transform2D()
		transform2d.origin = bullet.position
		Physics2DServer.body_set_state(bullet.body, Physics2DServer.BODY_STATE_TRANSFORM, transform2d)

		bullets.push_back(bullet)

func _process(_delta):
	# Order the CanvasItem to update every frame.
	update()

func _physics_process(delta):
	var transform2d = Transform2D()
	var offset = get_viewport_rect().size.x + 16
	for bullet in bullets:
		bullet.position.x -= bullet.speed * delta

		if bullet.position.x < -16:
			# The bullet has left the screen; move it back to the right.
			bullet.position.x = offset

		transform2d.origin = bullet.position

		Physics2DServer.body_set_state(bullet.body, Physics2DServer.BODY_STATE_TRANSFORM, transform2d)

# Instead of drawing each bullet individually in a script attached to each bullet,
# we are drawing *all* the bullets at once here.
func _draw():
	var offset = -bullet_image.get_size() * 0.5
	for bullet in bullets:
		draw_texture(bullet_image, bullet.position + offset)

# Perform cleanup operations (required to exit without error messages in the console).
func _exit_tree():
	for bullet in bullets:
		Physics2DServer.free_rid(bullet.body)

	Physics2DServer.free_rid(shape)
	bullets.clear()


Docs: 2D Platformer - Update Pause Mode Comments

I believe this comment got out-of-date (based on the fact that the `Stage` scene seems like something that was in an older version of the demo: https://github.com/godotengine/godot-demo-projects/blob/81441c42b79e38e54c2bc38acee3205f22431738/2d/platformer/Stage.tscn), so updated the commment to reflect the current state of the demo, along with some other information that I thought a developer might find helpful.


Randomized platform positions

Randomized platform positions

var bag

var segments = []
var last_visited_segment = -1

var SCREEN_WIDTH = Globals.get("display/width")
var SCREEN_HEIGHT = Globals.get("display/height")
var SCREEN_MARGIN = 200

var SEGMENT_SIZE = 60
var SEGMENT_LINE_HEIGHT = 36

var WALL_HEIGHT = 39
var WALL_HALF_WIDTH = 36

var platforms = [
    { 'width' : 100, 'template' : preload('res://scenes/map/platform_grass.xscn') }
]

var left_wall_template = preload('res://scenes/map/wall_left.xscn')
var right_wall_template = preload('res://scenes/map/wall_right.xscn')
var separator_template = preload('res://scenes/map/base_platform.xscn')
var playable_width = 0

func _init_bag(bag):
    self.bag = bag
    self.playable_width = self.SCREEN_WIDTH - (self.SCREEN_MARGIN * 2)
    self.reset_map()

func reset_map():
    for segment in self.segments:
        self.destroy_unused_segment(segment)
    self.segments.clear()

func generate_next_map_segment():
    var next_segment_index = self.segments.size()

    var new_segment = []

    new_segment = self.add_walls(new_segment)
    new_segment = self.add_separator(new_segment)
    new_segment = self.add_platforms(new_segment)

    self.segments.append(new_segment)
    self.apply_segment(next_segment_index)
    self.destroy_unused_segment_by_id(next_segment_index - 3)

func apply_segment(id):
    var segment_offset = id * self.SEGMENT_SIZE * self.SEGMENT_LINE_HEIGHT
    for object in self.segments[id]:
        self.bag.action_controller.attach_object(object.object)
        object.object.set_global_pos(Vector2(object.x, -object.y - segment_offset))

func destroy_unused_segment_by_id(id):
    if id < 0:
        return

    self.destroy_unused_segment(self.segments[id])

func destroy_unused_segment(segment):
    for object in segment:
        self.bag.action_controller.detach_object(object.object)
        object.object.queue_free()

    segment.clear()

func add_segment_object(segment, x, y, object):
    segment.append({
        'x' : x,
        'y' : y,
        'object' : object
    })

    return segment

func add_separator(segment):
    var separator_platform = self.separator_template.instance()
    segment = self.add_segment_object(segment, -10, self.SEGMENT_LINE_HEIGHT, separator_platform)
    return segment

func add_walls(segment):
    var height = 0
    var left_wall
    var right_wall
    var wall_offset
    var real_height

    while height < self.SEGMENT_SIZE:
        left_wall = self.left_wall_template.instance()
        right_wall = self.right_wall_template.instance()

        wall_offset = self.SCREEN_MARGIN - self.WALL_HALF_WIDTH
        real_height = (height + self.WALL_HEIGHT / 2) * self.SEGMENT_LINE_HEIGHT
        segment = self.add_segment_object(segment, wall_offset, real_height, left_wall)
        segment = self.add_segment_object(segment, wall_offset + self.playable_width + self.WALL_HALF_WIDTH * 2, real_height, right_wall)

        height = height + self.WALL_HEIGHT
    return segment

func update_segments(player_height):
    var segment_height = self.SEGMENT_SIZE * self.SEGMENT_LINE_HEIGHT
    var overflow = player_height % segment_height

    var segment_num = (player_height - overflow) / segment_height

    if overflow > 100 && segment_num > self.last_visited_segment:
        self.last_visited_segment = segment_num
        self.generate_next_map_segment()

func add_platforms(segment):
    var iterator = 2
    var last_iterator = 0

    randomize()

    while iterator < self.SEGMENT_SIZE - 3:
        segment = self.generate_single_platform(segment, iterator)

        last_iterator = iterator

        if randi() % 10 == 0:
            iterator = iterator + 2
        else:
            if randi() % 2 == 0:
                iterator = iterator + 3
            else:
                iterator = iterator + 4

    if last_iterator < self.SEGMENT_SIZE - 3:
        segment = self.generate_single_platform(segment, self.SEGMENT_SIZE - 3)

    return segment

func generate_single_platform(segment, iterator):
    var template = self.platforms[randi() % self.platforms.size()]
    var new_platform = template.template.instance()

    var horizontal_position = randi() % (self.playable_width - template.width)
    horizontal_position = horizontal_position + self.SCREEN_MARGIN + int(template.width / 2)

    segment = self.add_segment_object(segment, horizontal_position, (iterator + 1) * self.SEGMENT_LINE_HEIGHT, new_platform)

    return segment

var bag

var segments = []
var last_visited_segment = -1

var SCREEN_WIDTH = Globals.get("display/width")
var SCREEN_HEIGHT = Globals.get("display/height")
var SCREEN_MARGIN = 200

var SEGMENT_SIZE = 60
var SEGMENT_LINE_HEIGHT = 36

var WALL_HEIGHT = 39
var WALL_HALF_WIDTH = 36

var platforms = [
    { 'width' : 100, 'template' : preload('res://scenes/map/platform_grass.xscn') }
]

var left_wall_template = preload('res://scenes/map/wall_left.xscn')
var right_wall_template = preload('res://scenes/map/wall_right.xscn')
var separator_template = preload('res://scenes/map/base_platform.xscn')
var playable_width = 0

func _init_bag(bag):
    self.bag = bag
    self.playable_width = self.SCREEN_WIDTH - (self.SCREEN_MARGIN * 2)
    self.reset_map()

func reset_map():
    for segment in self.segments:
        self.destroy_unused_segment(segment)
    self.segments.clear()

func generate_next_map_segment():
    var next_segment_index = self.segments.size()

    var new_segment = []

    new_segment = self.add_walls(new_segment)
    new_segment = self.add_separator(new_segment)
    new_segment = self.add_platforms(new_segment)

    self.segments.append(new_segment)
    self.apply_segment(next_segment_index)
    self.destroy_unused_segment_by_id(next_segment_index - 3)

func apply_segment(id):
    var segment_offset = id * self.SEGMENT_SIZE * self.SEGMENT_LINE_HEIGHT
    for object in self.segments[id]:
        self.bag.action_controller.attach_object(object.object)
        object.object.set_global_pos(Vector2(object.x, -object.y - segment_offset))

func destroy_unused_segment_by_id(id):
    if id < 0:
        return

    self.destroy_unused_segment(self.segments[id])

func destroy_unused_segment(segment):
    for object in segment:
        self.bag.action_controller.detach_object(object.object)
        object.object.queue_free()

    segment.clear()

func add_segment_object(segment, x, y, object):
    segment.append({
        'x' : x,
        'y' : y,
        'object' : object
    })

    return segment

func add_separator(segment):
    var separator_platform = self.separator_template.instance()
    segment = self.add_segment_object(segment, -10, self.SEGMENT_LINE_HEIGHT, separator_platform)
    return segment

func add_walls(segment):
    var height = 0
    var left_wall
    var right_wall
    var wall_offset
    var real_height

    while height < self.SEGMENT_SIZE:
        left_wall = self.left_wall_template.instance()
        right_wall = self.right_wall_template.instance()

        wall_offset = self.SCREEN_MARGIN - self.WALL_HALF_WIDTH
        real_height = (height + self.WALL_HEIGHT / 2) * self.SEGMENT_LINE_HEIGHT
        segment = self.add_segment_object(segment, wall_offset, real_height, left_wall)
        segment = self.add_segment_object(segment, wall_offset + self.playable_width + self.WALL_HALF_WIDTH * 2, real_height, right_wall)

        height = height + self.WALL_HEIGHT
    return segment

func update_segments(player_height):
    var segment_height = self.SEGMENT_SIZE * self.SEGMENT_LINE_HEIGHT
    var overflow = player_height % segment_height

    var segment_num = (player_height - overflow) / segment_height

    if overflow > 100 && segment_num > self.last_visited_segment:
        self.last_visited_segment = segment_num
        self.generate_next_map_segment()

func add_platforms(segment):
    var iterator = 2
    var new_platform

    while iterator < self.SEGMENT_SIZE - 1:
        new_platform = self.platforms[0].template.instance()

        segment = self.add_segment_object(segment, 400, (iterator + 1) * self.SEGMENT_LINE_HEIGHT, new_platform)
        iterator = iterator + 2

    return segment