Comment gérer les règles de Warhammer 40K avec les Design Pattern
par
, 10/07/2022 à 14h11 (5549 Affichages)
C'est connu, il y a trop de règles à Warhammer 40K;
C'est d'ailleurs ce qui rebute.
Mais les règles deviennent importantes si on espère faire un calcul de probabilité.
Heureusement, il existe les Designs patterns qui vont permettre d'un peu simplifier le problème, et passer à un problème bourratif d'implémentation de règles.
Déjà, commençons par un point: Une figurine n'a pas de règles (excepté celles de base).
Si on prend un space marine, il a les règles de spaces-marines (Et il ne connaitrons pas la peur, les doctrines...).
Les règles du space marine s'ajoute aux règles de bases (aucune à la base, je précise, ça va avoir son importance).
Et puis il y a les chapitres. Comme par exemple les Dark Angels.
Les règles des Dark Angels (selon qu'il soit de la Deathwing, de la Ravenwing ou ni l'un, ni l'autre) qui s'ajoute aux règles de marines qui s'ajoute aux règles "de bases".
On remarque donc qu'on a une notion d'héritage
Ceci étant fait, il va falloir faire un point sur les Designs Patterns.
Les Designs Patterns
Il y a plusieurs approches.
La plus connue, dont on va parler est le GOF.
Effectivement, Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides (Gang of Four, gang des 4) ont écrit un livre de référence sur les patrons architecturaux en langage objet. C'est de ça que l'on va parler.
A noter, que pour la théorie, il y a le GRAPS Et dans la vie du codeur de base, il y a SOLID.
SOLID est la philosophie qui doit guider le développement au quotidien. Mais on va voir que GOF et SOLID, si au premier abord n'ont pas la même approche, sont en fait complémentaires.
Partons d'un principe de base
On va faire plus simple. Je pense qu'il y a un principe de base pour faire une bonne architecture.
"Je sais ce que tu fais, mais je n'ai pas envie de savoir comment tu es codé".
Effectivement, le programme qui va calculer les proba a juste besoin des règles (et proflls). Il a besoin de ce que l'on appelle en UML l'interface, c'est à dire la signature des méthodes.
Il n'a pas besoin de savoir comment c'est implémenté.
C'est d'ailleurs pour ça que dans Effective Java Joshua Bloch explique qu'il faut préférer les classes abstraites aux interfaces.
Ce principe est identique au modèle Réseau OSI.
Ce qui tombe bien, l'Interface, notion en particulier UML a une réalité en Java. C'est un type.
Premier contrat, l'identifiable
On va donc définir nos interfaces, que je vais nommer contrat.
Et comme dans warhammer 40K tout le monde a sa petite règle (la figurine, le bâtiment,le pouvoir psychique et l'aura...), on va donc définir l'identifiable:
L'identifiable est identifiable par son... identifiant. Ce qui pourra être utilse quand on le mettra en Base de données (où néanmoins, avec Hibernate, on préfère les Long).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package com.calculateur.warhammer.data.identifiable; /** * Cette interface permet de contractualiser un identifiable. Celui-ci est identifié par un id entier * @author phili * */ public interface IIdentifiable { /** * * @return L'id de l'identifiable */ Integer getId(); }
Deux identifiables vont être important pour le calcul:
Le profil:
Le bâtiment:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98 package com.calculateur.warhammer.data.identifiable; import java.util.Set; import com.calculateur.warhammer.data.enumeration.EProfil; /** * Un profil à warhammer 40K * @author phili * */ public interface IProfil extends IIdentifiable{ /** * * @return Le mouvelment */ Integer getMouvement(); /** * * @return La CC */ Integer getCapaciteDeCombat(); /** * * @return La CT */ Integer getCapaciteTir(); /** * * @return La Force */ Integer getForce(); /** * * @return L'endurance */ Integer getEndurance(); /** * * @return Le nombre de point de vie */ Integer getNombrePointDeVie(); /** * * @return Le nombre d'attaque de base */ Integer getNombreAttaque(); /** * * @return Le nombre d'attaque par D3 */ Integer getNombreAttaqueParD3(); /** * * @return Le nombre d'attaque par D6 */ Integer getNombreAttaqueParD6(); /** * * @return Le commandement */ Integer getCommandement(); /** * * @return La sauvegarde */ Integer getSauvegarde(); /** * * @return La sauvegarde Invulnérable */ Integer getSauvegardeInvulnerable(); /** * * @return Les types du profil */ Set<EProfil> getTypesProfils(); /** * * @return Vrai si c'est un volant */ Boolean isVolant(); }
Ainsi que les armes:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 package com.calculateur.warhammer.data.identifiable; /** * Cette interface représente un batiment à 40K * @author phili * */ public interface IBatiment extends IIdentifiable{ /** * * @return Vrai si le batiment est identifiable */ boolean isDefendable(); /** * * @return Vrai si le batiment est un couvert dense */ boolean isCouvertDense(); /** * * @return Vrai si le batiment est un couvert léger */ boolean isCouvertLeger(); /** * * @return Vrai si le batiment est un couvert lourd */ boolean isCouvertLourd(); /** * * @return Vrai si bâtiment est exaltant */ boolean isExaltant(); }
Notez qu'il existe des armes de corps à corps et de tir.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 package com.calculateur.warhammer.data.identifiable; /** * L'arme utilisé * @author phili * */ public interface IArme extends IIdentifiable{ /** * * @return La PA de l'arme */ Integer getPA(); /** * * @return Le nombre de D3 qu'il faut lancer pour avoir la PA */ Integer getPAParD3(); /** * * @return Le nomnbre de D6 qu'il faut lancer pour avoir la PA */ Integer getPAParD6(); /** * * @return Les dégats de base */ Integer getDegâts(); /** * * @return Le nombre de D3 qu'il faut avoir pour avoir les dégâts */ Integer getDegatsParD3(); /** * * @return Nombre d'attaque par D6 */ Integer getDegatsParD6(); }
Dans le contexte des interfaces Java, on va composer.
On introduit donc les armes de tir:
Et l'arme de corps à corps:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78 package com.calculateur.warhammer.data.identifiable; import com.calculateur.warhammer.data.enumeration.EArmeTir; /** * Les spécifications pour l'arme de tir * @author phili * */ public interface IArmeDeTir extends IArme{ /** * * @return Portée minimale de l'arme */ Integer getPorteeMinimale(); /** * * @return Portée maximale de l'arme */ Integer getPorteeMaximale(); /** * * @return Portée maximale de l'arme */ EArmeTir getType(); /** * * @return Nombre d'attaque de base */ Integer getNombreAttaqueBase(); /** * * @return Le nombre de D3 qu'il faut lancer pour attaque */ Integer getNombreAttaqueD3(); /** * * @return Le nombre de D6 qu'il faut lancer pour une attaque */ Integer getNombreAttaqueD6(); /** * * @return le nombre minimum qu'il faut lancer pour un daka */ Integer getNombreDakaMin(); /** * * @return le nombre maximum qu'il faut lancer pour un Daka */ Integer getNombreDakaMax(); /** * * @return Vrai si il faut faire un jet de touche */ Boolean isJetTouche(); /** * * @return Vrai si l'arme est Blast */ Boolean isBlast(); /** * * @return Vrai si l'arme peut tirer sans ligne de vue */ boolean isTirIndirect(); }
Contrat pour les règles
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 package com.calculateur.warhammer.data.identifiable; /** * Cette Interface représente les armes de corps à corps * @author phili * */ public interface IArmeDeCorpsACorps extends IArme{ /** * * @return Bonus d'attaques aditionnel */ Integer getBonusAttaqueFixe(); /** * * @return Bonus aditionnel donné par un nombre de D3 */ Integer getBonusAttaqueD3(); /** * * @return Bonus aditionnel donné par un nombre de D6 */ Integer getBonusAttaqueD6(); /** * * @return Bonus Additionnel en force donné par l'arme (F + bonus) */ Integer getBonusAditionForce(); /** * * @return Bonus multiplicatif donné par l'arme Bonus * (F + bonus aditionnel) */ Integer getBonusMultiplicationForce(); /** * * @return Bonus Additionnel en force donné par l'arme après avoir jeté un vombre de D3 */ Integer getBonusAditionForceD3(); /** * * @return Bonus Additionnel en force donné par l'arme après avoir jeté un vombre de D6 */ Integer getBonusAditionForceD6(); }
On va donc définir un contrat pour les règles.
Pour commencer, une règles est pour un identifiable. Notez que contrairement à ce que j'ai rendu pour le SMB116, on traite les bâtiments comme ayant des règles.
On a donc la règle, avec son identifiable (pour la retrouver, bien évidement).
La pratique a montré qu'il fallait décomposer les règles de l'attaquant et du défenseur, au moins pour limiter la taille des classes, et pour avoir une meilleur lisibilité du code.package com.calculateur.warhammer.data.regles;
import com.calculateur.warhammer.data.identifiable.IIdentifiable;
/**
* Règle spéciale pour un identifiable
* @author phili
*
* @param <I> Identifiable auquel s'applique la règle
*/
public interface IRegle <I extends IIdentifiable>{
/**
*
* @return L'identifiable à laquelle la règle s'applique
*/
I getIdentifiable();
}
Donc on a:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154 package com.calculateur.warhammer.data.regles; import java.util.Map; import java.util.Set; /** * Règle d'attaque * @author phili */ public interface IRegleAttaque{ //Règle mouvement /** * * @return Vrai si celui-ci ignore les malus du mouvement d'avance au tir */ boolean isIgnoreMouvementAvanceAuTir(); /** * * @return Vrai si celui-ci ignore les malus du mouvement normal au tir */ boolean isIgnoreMouvementNormalAuTir(); /** * * @return Vrai si pour une arme à tir rapide, la figurine tir toujours 2 fois */ boolean isMaitriseArmeTirRapide(); //Jet de touche /** * * @return Vrai si le jet de touche a un bonus. */ Integer getBonusJetTouche(); /** * * @return Les scores relançables */ Set<Integer> getScoreJetToucheRelancable(); /** * * @return Jet minimum pour toucher */ Integer getJetMinimumPourToucher(); /** * * @return La map qui donne un jet de blessures mortelles, et on s'arrête après le jet de touche. <br/> * Le second chiffre donne le nombre de blessures mortelles */ Map<Integer, Integer> getMapJetToucheBlessuresMortelleEtOnArrete(); /** * * @return La Map de blessures Mortelles et on continue la séquence. C'est donc des blessures mortelles en plus.<br/> * Le second chiffre est le nombre de blessures mortelles */ Map<Integer,Integer> getMapJetToucheBlessuresMortellesEtContinue(); /** * * @return La map score qui donne des attaques en plus. Le second chiffre est le nombre d'attaqye */ Map<Integer,Integer> getMapJetToucheAttaqueSupplementaire(); //Force /** * * @return Le bonus de force de l'adversaire */ Integer getBonusForce(); //Endurance /** * * @return Malus appliqué à une cible sur l'endurance */ Integer getMalusEndurance(); //Jet de blessures /** * * @return le bonus au jet de touche */ Integer getBonusJetBlessures(); /** * * @return Jet de blessures relançable */ Set<Integer> getScoreJetBlessureRelancable(); /** * * @return Jet minimum pour blesser */ Integer getJetMinumumPourBlesser(); /** * * @return La map qui donne un jet de blessures mortelles, et on s'arrête après le jet de blessure. <br/> * Le second chiffre donne le nombre de blessures mortelles */ Map<Integer, Integer> getMapJetBlessuresBlessuresMortelleEtOnArrete(); /** * * @return La Map de blessures Mortelles et on continue la séquence. C'est donc des blessures mortelles en plus.<br/> * Le second chiffre est le nombre de blessures mortelles */ Map<Integer,Integer> getMapJetBlessuresBlessuresMortellesEtContinue(); /** * * @return La map qui pour un jet de touche, donne un bonus à la PA.<br/> * Le second chiffre est le bonus à la PA */ Map<Integer,Integer> getMapJetBlessureBonusPA(); //Attaques Integer getBonusNombreAttaque(); //Commandement /** * * @return Le malus que la règle donne aux test d'attritions */ Integer getMalusSurAttrition(); //Sauvegarde /** * * @return Vrai si l'attaquant ignore la sauvegarde invulnérable */ boolean isIgnoreSauvegardeInvulnerable(); /** * * @return Vrai si on fait ignorer la sauvegarde de couvert */ boolean isIgnoreSauvegardeCouvert(); //Se relève /** * * @return Vrai si la cible ne peut pas ignorer les blessures */ boolean isIgnoreIgnoreBlessure(); }A ce stade, on ne va pas implémenter pour chaque faction (il y en a trop) mais pour des cas généraux.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121 package com.calculateur.warhammer.data.regles; import java.util.Set; import com.calculateur.warhammer.data.enumeration.EBlessure; /** * Règle pour se défendre * @author phili * */ public interface IRegleDefense { //Jet de touche /** * * @return Malus que subit l'attaquant au jet de touche */ Integer getMalusJetTouche(); /** * * @return Le jet minimum pour être touché */ Integer getJetMinimumPourEtreTouche(); //Force /** * * @return Malus que l'attaquant subit à la sauvegarde */ Integer getMalusForce(); //Endurance /** * * @return Bonus en endurance dont bénéficie le défenseur */ Integer getBonusEndurance(); //Jet blessure /** * * @return Jet minimum pour être blessé */ Integer getJetMinimumPourEtreBlesse(); /** * * @return Malus au jet de blessure */ Integer getMalusJetBlessure(); //PV /** * * @return Le nombre maximum de PV que la figurine peut perdre par phase */ Integer getNombrePointsViesMaximumPerdable(); //Attaques /** * * @return Malus que l'attaquant subit au nombre d'attaque. */ Integer getMalusNombreAttaque(); //Sauvegarde /** * * @return Vrai si il y a une sauvegarde de couvert */ boolean isSauvegardeCouvert(); //Commandement /** * * @return Vrai si la défense ignore le test de commandement */ boolean isIgnoreTestCommandement(); /** * * @return Bonus aux tests de commandement */ Integer getBonusCommandement(); /** * * @return Vrai si le test d'attrition est ignoré */ boolean isIgnoreTestAttrition(); /** * * @return Vrai si les malus au tests d'attrition sont ignorés */ boolean isIgnoreMalusAttrition(); //Se relève /** * * @return Le score pour se relever après la mort */ Integer getJetSeReleve(); /** * * @return Les relance du jet pour se relever */ Set<Integer> getRelancesSeReleve(); //Ignore blessures /** * * @param typeBlessures * @return Le jet pour ignorer une blessure */ Integer getJetSoreIgnoreBlessure(EBlessure typeBlessures); }
A la base, pas de règles: Design Pattern Template Method
Comme dit précédemment, à la base, on applique pas de règle spéciales, mais celle du livre de base.
Ce qui permet de faire une implémentation pour la base, soit:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143 package com.calculateur.warhammer.data.regles; import java.util.Collections; import java.util.Map; import java.util.Set; import com.calculateur.warhammer.data.identifiable.IIdentifiable; /** * Règles d'attaque de base pour un identifiable (c'est à dire aucune) * * @author phili * */ public class RegleAttaqueBase<I extends IIdentifiable> implements IRegle<I>, IRegleAttaque { private static final Integer NO_BONUS = 0; private static final Integer NO_JET_MINIMUM = 0; private final I identifiable; public RegleAttaqueBase(I identifiable) { this.identifiable = identifiable; } @Override public boolean isIgnoreMouvementAvanceAuTir() { return false; } @Override public boolean isIgnoreMouvementNormalAuTir() { return false; } @Override public boolean isMaitriseArmeTirRapide() { return false; } @Override public Integer getBonusJetTouche() { return NO_BONUS; } @Override public Set<Integer> getScoreJetToucheRelancable() { return Collections.emptySet(); } @Override public Integer getJetMinimumPourToucher() { return NO_JET_MINIMUM; } @Override public Map<Integer, Integer> getMapJetToucheBlessuresMortelleEtOnArrete() { return Collections.emptyMap(); } @Override public Map<Integer, Integer> getMapJetToucheBlessuresMortellesEtContinue() { return Collections.emptyMap(); } @Override public Map<Integer, Integer> getMapJetToucheAttaqueSupplementaire() { return Collections.emptyMap(); } @Override public Integer getBonusForce() { return NO_BONUS; } @Override public Integer getMalusEndurance() { return NO_BONUS; } @Override public Integer getBonusJetBlessures() { return NO_BONUS; } @Override public Set<Integer> getScoreJetBlessureRelancable() { return Collections.emptySet(); } @Override public Integer getJetMinumumPourBlesser() { return NO_JET_MINIMUM; } @Override public Map<Integer, Integer> getMapJetBlessuresBlessuresMortelleEtOnArrete() { return Collections.emptyMap(); } @Override public Map<Integer, Integer> getMapJetBlessuresBlessuresMortellesEtContinue() { return Collections.emptyMap(); } @Override public Map<Integer, Integer> getMapJetBlessureBonusPA() { return Collections.emptyMap(); } @Override public Integer getBonusNombreAttaque() { return NO_BONUS; } @Override public Integer getMalusSurAttrition() { return NO_BONUS; } @Override public I getIdentifiable() { return identifiable; } @Override public boolean isIgnoreSauvegardeInvulnerable() { return false; } @Override public boolean isIgnoreSauvegardeCouvert() { return false; } @Override public boolean isIgnoreIgnoreBlessure() { return false; } }On a donc de fait un Template Method, vu que les règles des Spaces Marines vont hériter, des règles de bases...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 package com.calculateur.warhammer.data.regles; import java.util.Collections; import java.util.Set; import com.calculateur.warhammer.data.enumeration.EBlessure; import com.calculateur.warhammer.data.identifiable.IIdentifiable; public class RegleDefenseBase<I extends IIdentifiable> implements IRegle<I>,IRegleDefense { private static final Integer NO_BONUS = 0; private static final Integer NO_JET_MINIMUM = 0; private final I identifiable; public RegleDefenseBase(I identifiable) { this.identifiable = identifiable; } @Override public Integer getMalusJetTouche() { return NO_BONUS; } @Override public Integer getJetMinimumPourEtreTouche() { return NO_JET_MINIMUM; } @Override public Integer getMalusForce() { return NO_BONUS; } @Override public Integer getBonusEndurance() { return NO_BONUS; } @Override public Integer getJetMinimumPourEtreBlesse() { return NO_JET_MINIMUM; } @Override public Integer getMalusJetBlessure() { return NO_BONUS; } @Override public Integer getNombrePointsViesMaximumPerdable() { return NO_BONUS; } @Override public Integer getMalusNombreAttaque() { return NO_BONUS; } @Override public boolean isSauvegardeCouvert() { return false; } @Override public boolean isIgnoreTestCommandement() { return false; } @Override public Integer getBonusCommandement() { return NO_BONUS; } @Override public boolean isIgnoreTestAttrition() { return false; } @Override public boolean isIgnoreMalusAttrition() { return false; } @Override public Integer getJetSeReleve() { return NO_BONUS; } @Override public Set<Integer> getRelancesSeReleve() { return Collections.emptySet(); } @Override public Integer getJetSoreIgnoreBlessure(EBlessure typeBlessures) { return NO_JET_MINIMUM; } @Override public I getIdentifiable() { return identifiable; } }
En général, ce qui est implémenté dans un Template Method dépends de spécifications qui dépendent du contexte.
C'est pour ça que ces "spécificatons" sont des méthodes abstraites. Le Template Méthode est donc une classe abstraite avec des méthodes abstraites pour pouvoir prendre en compte les spécifications qui seront dans les classes filles.
Ce n'est pas le cas ici.
Un autre problème, c'est qu'il risque dans certains cas, il risque d'y avoir beaucoup de classes filles.
Pour gérer l'ensemble des règles: Le Design Pattern Décorateur
Dans un cas réél, on est un space marine, avec ses règles, dans un bâtiment qui a ses règles, avec des chefs, qui eux aussi donnent leurs règles...
Pas de problème, il existe aussi un Design Pattern pour ça: le Design Pattern Décorateur.
On va passer la liste des règles, et on au final, l'utilisateur ne va voir qu'une seule règle, celles à appliquer.
Ici, la règle vue par l'utilisateur (donc celle à appliquer) est décorée par un ensemble de règle.
Ce qui donne:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199 package com.calculateur.warhammer.data.regles; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; /** * Un décorateur qui prend un ensemble de règles d'attaque et fait un traitement * pour sortir la bonne règle au bon moment. * * @author phili * */ public class RegleAttaqueDecorateur implements IRegleAttaque { private final List<IRegleAttaque> regles; public RegleAttaqueDecorateur(List<IRegleAttaque> regles) { this.regles = regles; } @Override public boolean isIgnoreMouvementAvanceAuTir() { return regles.stream().anyMatch(r -> r.isIgnoreMouvementAvanceAuTir()); } @Override public boolean isIgnoreMouvementNormalAuTir() { return regles.stream().anyMatch(r -> r.isIgnoreMouvementNormalAuTir()); } @Override public boolean isMaitriseArmeTirRapide() { return regles.stream().anyMatch(r -> r.isMaitriseArmeTirRapide()); } @Override public Integer getBonusJetTouche() { return regles.stream().mapToInt(r -> r.getBonusJetTouche()).sum(); } @Override public Set<Integer> getScoreJetToucheRelancable() { Set<Integer> sReturn = new HashSet<>(); regles.stream().forEach(r -> sReturn.addAll(r.getScoreJetToucheRelancable())); return sReturn; } @Override public Integer getJetMinimumPourToucher() { Integer result = 0; Integer aResult = regles.stream().filter(r -> r.getJetMinimumPourToucher() > 0) .mapToInt(r -> r.getJetMinimumPourToucher()).min().getAsInt(); return (aResult != null) ? aResult : result; } @Override public Map<Integer, Integer> getMapJetToucheBlessuresMortelleEtOnArrete() { Map<Integer, Integer> mReturn = new HashMap<>(); Integer value; for (IRegleAttaque regle : regles) { for (Entry<Integer, Integer> entry : regle.getMapJetToucheBlessuresMortelleEtOnArrete().entrySet()) { value = mReturn.computeIfAbsent(entry.getKey(), k -> 0); value += entry.getValue(); mReturn.put(entry.getKey(), value); } } return mReturn; } @Override public Map<Integer, Integer> getMapJetToucheBlessuresMortellesEtContinue() { Map<Integer, Integer> mReturn = new HashMap<>(); Integer value; for (IRegleAttaque regle : regles) { for (Entry<Integer, Integer> entry : regle.getMapJetToucheBlessuresMortellesEtContinue().entrySet()) { value = mReturn.computeIfAbsent(entry.getKey(), k -> 0); value += entry.getValue(); mReturn.put(entry.getKey(), value); } } return mReturn; } @Override public Map<Integer, Integer> getMapJetToucheAttaqueSupplementaire() { Map<Integer, Integer> mReturn = new HashMap<>(); Integer value; for (IRegleAttaque regle : regles) { for (Entry<Integer, Integer> entry : regle.getMapJetToucheAttaqueSupplementaire().entrySet()) { value = mReturn.computeIfAbsent(entry.getKey(), k -> 0); value += entry.getValue(); mReturn.put(entry.getKey(), value); } } return mReturn; } @Override public Integer getBonusForce() { return regles.stream().mapToInt(r -> r.getBonusForce()).sum(); } @Override public Integer getMalusEndurance() { return regles.stream().mapToInt(r -> r.getMalusEndurance()).sum(); } @Override public Integer getBonusJetBlessures() { return regles.stream().mapToInt(r -> r.getBonusJetBlessures()).sum(); } @Override public Set<Integer> getScoreJetBlessureRelancable() { Set<Integer> sReturn = new HashSet<>(); regles.stream().forEach(r -> sReturn.addAll(r.getScoreJetBlessureRelancable())); return sReturn; } @Override public Integer getJetMinumumPourBlesser() { Integer result = 0; Integer aResult = regles.stream().filter(r -> r.getJetMinumumPourBlesser() > 0) .mapToInt(r -> r.getJetMinumumPourBlesser()).min().getAsInt(); return (aResult != null) ? aResult : result; } @Override public Map<Integer, Integer> getMapJetBlessuresBlessuresMortelleEtOnArrete() { Map<Integer, Integer> mReturn = new HashMap<>(); Integer value; for (IRegleAttaque regle : regles) { for (Entry<Integer, Integer> entry : regle.getMapJetBlessuresBlessuresMortelleEtOnArrete().entrySet()) { value = mReturn.computeIfAbsent(entry.getKey(), k -> 0); value += entry.getValue(); mReturn.put(entry.getKey(), value); } } return mReturn; } @Override public Map<Integer, Integer> getMapJetBlessuresBlessuresMortellesEtContinue() { Map<Integer, Integer> mReturn = new HashMap<>(); Integer value; for (IRegleAttaque regle : regles) { for (Entry<Integer, Integer> entry : regle.getMapJetBlessuresBlessuresMortellesEtContinue().entrySet()) { value = mReturn.computeIfAbsent(entry.getKey(), k -> 0); value += entry.getValue(); mReturn.put(entry.getKey(), value); } } return mReturn; } @Override public Map<Integer, Integer> getMapJetBlessureBonusPA() { Map<Integer, Integer> mReturn = new HashMap<>(); Integer value; for (IRegleAttaque regle : regles) { for (Entry<Integer, Integer> entry : regle.getMapJetBlessureBonusPA().entrySet()) { value = mReturn.computeIfAbsent(entry.getKey(), k -> 0); value += entry.getValue(); mReturn.put(entry.getKey(), value); } } return mReturn; } @Override public Integer getBonusNombreAttaque() { return regles.stream().mapToInt(r -> r.getBonusNombreAttaque()).sum(); } @Override public Integer getMalusSurAttrition() { return regles.stream().mapToInt(r -> r.getMalusSurAttrition()).sum(); } @Override public boolean isIgnoreSauvegardeInvulnerable() { return regles.stream().anyMatch(r -> r.isIgnoreSauvegardeInvulnerable()); } @Override public boolean isIgnoreSauvegardeCouvert() { return regles.stream().anyMatch(r -> r.isIgnoreSauvegardeInvulnerable()); } @Override public boolean isIgnoreIgnoreBlessure() { return regles.stream().anyMatch(r -> r.isIgnoreIgnoreBlessure()); } }Et les bâtiment
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115 package com.calculateur.warhammer.data.regles; import java.util.HashSet; import java.util.List; import java.util.Set; import com.calculateur.warhammer.data.enumeration.EBlessure; /** * Décorateur de règle pour la défense * @author phili * */ public class RegleDefenseDecorateur implements IRegleDefense{ private final List<IRegleDefense> regles; public RegleDefenseDecorateur(List<IRegleDefense> regles) { this.regles = regles; } @Override public Integer getMalusJetTouche() { return regles.stream().mapToInt(r -> r.getMalusJetTouche()).sum(); } @Override public Integer getJetMinimumPourEtreTouche() { Integer iReturn = 0; Integer result = regles.stream().filter(r -> r.getJetMinimumPourEtreTouche() > 0).mapToInt(r -> r.getJetMinimumPourEtreTouche()).max().getAsInt(); return (result != null)?result:iReturn; } @Override public Integer getMalusForce() { return regles.stream().mapToInt(r -> r.getMalusForce()).sum(); } @Override public Integer getBonusEndurance() { return regles.stream().mapToInt(r -> r.getBonusEndurance()).sum(); } @Override public Integer getJetMinimumPourEtreBlesse() { Integer iReturn = 0; Integer result = regles.stream().filter(r -> r.getJetMinimumPourEtreBlesse() > 0).mapToInt(r -> r.getJetMinimumPourEtreBlesse()).max().getAsInt(); return (result != null)?result:iReturn; } @Override public Integer getMalusJetBlessure() { return regles.stream().mapToInt(r -> r.getMalusJetBlessure()).sum(); } @Override public Integer getNombrePointsViesMaximumPerdable() { Integer iReturn = 0; Integer result = regles.stream().filter(r -> r.getNombrePointsViesMaximumPerdable() > 0).mapToInt(r -> r.getNombrePointsViesMaximumPerdable()).min().getAsInt(); return (result != null)?result:iReturn; } @Override public Integer getMalusNombreAttaque() { return regles.stream().mapToInt(r -> r.getMalusNombreAttaque()).sum(); } @Override public boolean isSauvegardeCouvert() { return regles.stream().anyMatch(r -> r.isSauvegardeCouvert()); } @Override public boolean isIgnoreTestCommandement() { return regles.stream().anyMatch(r -> r.isIgnoreTestCommandement()); } @Override public Integer getBonusCommandement() { return regles.stream().mapToInt(r -> r.getBonusCommandement()).sum(); } @Override public boolean isIgnoreTestAttrition() { return regles.stream().anyMatch(r -> r.isIgnoreTestAttrition()); } @Override public boolean isIgnoreMalusAttrition() { return regles.stream().anyMatch(r -> r.isIgnoreMalusAttrition()); } @Override public Integer getJetSeReleve() { Integer iReturn = 0; Integer result = regles.stream().filter(r -> r.getJetSeReleve() > 0).mapToInt(r -> r.getJetSeReleve()).min().getAsInt(); return (result != null)?result:iReturn; } @Override public Set<Integer> getRelancesSeReleve() { Set<Integer> sReturn = new HashSet<>(); regles.stream().forEach(r -> sReturn.addAll(r.getRelancesSeReleve())); return sReturn; } @Override public Integer getJetSoreIgnoreBlessure(EBlessure typeBlessures) { Integer iReturn = 0; Integer result = regles.stream().filter(r -> r.getJetSoreIgnoreBlessure(typeBlessures) > 0).mapToInt(r -> r.getJetSoreIgnoreBlessure(typeBlessures)).min().getAsInt(); return (result != null)?result:iReturn; } }
Dans ce que j'ai rendu pour SMB116, je différentiais dans le calcul le bâtiment. Et c'était juste infernal.
Du coup, dans la reprise, un bâtiment, c'est juste un identifiable qui a ses propres règles.
Il suffit donc de faire une implémentation pour le bâtiment:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 package com.calculateur.warhammer.data.regles; import com.calculateur.warhammer.data.enumeration.EMouvement; import com.calculateur.warhammer.data.enumeration.ESimule; import com.calculateur.warhammer.data.identifiable.IBatiment; /** * Règle pour l'attaquant étant dans un bâtiment * @author phili * */ public class RegleAttaqueBatiment extends RegleAttaqueBase<IBatiment> { private final IBatiment batiment; private final ESimule simule; private final EMouvement mouvementPrecedent; private final boolean isTirContreChargeAvant; private static final Integer BONUS_TOUCHE = -1; private static final Integer NO_BONUS = 1; public RegleAttaqueBatiment(IBatiment batiment, ESimule simule, EMouvement mouvementPrecedent, boolean isTirContreChargeAvant) { super(batiment); this.batiment = batiment; this.simule = simule; this.mouvementPrecedent = mouvementPrecedent; this.isTirContreChargeAvant = isTirContreChargeAvant; } @Override public Integer getBonusJetTouche() { Integer iReturn; if (mouvementPrecedent == EMouvement.IMMOBILE) { boolean isDefendable = batiment.isDefendable() && (simule == ESimule.TIR_CONTRE_CHARGE || (!isTirContreChargeAvant && simule == ESimule.CORPS_A_CORPS_APRES_ACTIVATION)); boolean isCouvertLourd = batiment.isCouvertLourd() && simule == ESimule.CORPS_A_CORPS_APRES_ACTIVATION && mouvementPrecedent == EMouvement.IMMOBILE; iReturn = (isDefendable || isCouvertLourd)?BONUS_TOUCHE:NO_BONUS; } else { iReturn = NO_BONUS; } return iReturn; } }Design Pattern Stratégie
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 package com.calculateur.warhammer.data.regles; import com.calculateur.warhammer.data.enumeration.ESimule; import com.calculateur.warhammer.data.identifiable.IBatiment; public class RegleDefenseBatiment extends RegleDefenseBase<IBatiment>{ private final IBatiment batiment; private final ESimule simule; private static final Integer BONUS_TOUCHE = -1; private static final Integer BONUS_COMMANDEMENT = 1; private static final Integer NO_BONUS = 0; public RegleDefenseBatiment(IBatiment batiment, ESimule simule) { super(batiment); this.batiment = batiment; this.simule = simule; } @Override public Integer getMalusJetTouche() { return (simule == ESimule.TIR_PHASE_TIR && batiment.isCouvertDense()) ? BONUS_TOUCHE : NO_BONUS; } @Override public boolean isSauvegardeCouvert() { return simule == ESimule.TIR_PHASE_TIR ? batiment.isCouvertLeger() : false; } @Override public Integer getBonusCommandement() { return batiment.isExaltant() ? BONUS_COMMANDEMENT : NO_BONUS; } }
Au final, on a aussi utilisé le Design Pattern Stratégie.
L'utilisateur ne voit que la règle, mais on l'obtient de différente façon, en changeant de stratégie en somme.
Et en cas de nouvelles règles (ou d'oubli...)
Effectivement, la dernière fois, j'avais oublié que si ils chargent ou si ils sont chargés, les marines on une attaque de plus.
Et pour les démons, les rumeurs parlent d'un nouveau type de sauvegarde.
Pas de problème, on l'ajoute dans l'interface et on l'implémente dans les deux classes (le décorateur et la base).
Bon, maintenant, il faut obtenir la règle: le Design Pattern Factory
Pour obtenir la règle, on va utiliser le Design Pattern Factory.
Ici, on a pas besoin, à ce stade, d'implémenter. On définit un contrat.
Définir ce contrat sera important pour pouvoir faire l'injection de dépendance quand on ajoutera Spring.
Soit:
Même si je ne vais pas parler des exceptions Java (pour le moment), on note que la Factory jette une exception utilisateur (FunctionnalExeption) et une si elle n'arrive pas à fabriquer la règle (FactoryReglesException).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 package com.calculateur.warhammer.data.regles.factory; import com.calculateur.warhammer.base.exception.FunctionnalExeption; import com.calculateur.warhammer.data.regles.IRegleAttaque; import com.calculateur.warhammer.data.regles.IRegleDefense; /** * Factory qui va créer la règle * @author phili * */ public interface FactoryRegle { /** * * @param parametres Les paramère nécessaires propre à la faction * @return Les régles d'attaques de la faction * @throws FactoryReglesException Si une exception s'est produite lors de la fabrication des rècles * @throws FunctionnalExeption Si après vérification, il manque une règle */ IRegleAttaque getRegleAttaque(String parametres)throws FactoryReglesException,FunctionnalExeption; /** * * @param parametres Les paramère nécessaires propre à la faction * @return Les régles de défense la faction * @throws FactoryReglesException Si une exception s'est produite lors de la fabrication des rècles * @throws FunctionnalExeption Si après vérification, il manque une règle */ IRegleDefense getRegleDefense(String parametres)throws FactoryReglesException,FunctionnalExeption; }
On passe sous forme de String (comme "isFigurinePerdu : true" par exemple) pour pouvoir paramètrer la règle.
On a aussi, comme c'est d'ailleurs l'usage en Java une Factoy pour la Factory.
On utilisera sans doute l'introspection mais ça, c'est une autre histoire.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package com.calculateur.warhammer.data.regles.factory; /** * Factory pour fabriquer la Factory de règle. * @author phili * */ public interface FactoryFactoryRegle { /** * * @param aClass Class de factory * @return La factory de règles * @throws FactoryReglesException */ FactoryRegle getFactory(String aClass)throws FactoryReglesException; }