Javaquarium

4 ans et 3 mois après, il est toujours là, et toujours aussi difficile !

a marqué ce sujet comme résolu.

Salut !

J'ai commencé un premier essai en Java pour appliquer l'ECS mais je suis arrivé à un hic.

Si j'applique un ECS complet, je crée des composants, des systèmes et des entités. Les entités contiennent différents composants pour définir ce qu'elles sont et les systèmes agissent sur ces entités en modifiant ces composants.
Les systèmes représentent les comportements, les composants les caractéristiques d'une entité et l'entité… l'entité.

Mais pour les comportements reproducteurs, par exemple, je n'arrive pas à trouver de solution viable.
Imaginons une entité qui contient le composant HermaphroditeOpportuniste. Le système ComportementReproducteur doit comprendre qu'il s'agit d'une entité hermaphrodite opportuniste.
1er hic : dans le système ComportementReproducteur, je ne vois pas comment faire autrement qu'avec une série de conditions pour faire reproduire l'entité en fonction de son composant de reproduction (ici HermaphroditeOpportuniste). Si j'ajoute un nouveau type de reproduction, j'ajoute une nouvelle condition avec le code correspondant dans la méthode evolve() (ou update(), ou compute(), fin bref, nommez-là comme vous voulez). On ne peut donc pas vraiment ajouter un nouveau type de reproduction de façon simple et efficace. 2e hic : si j'utilise une suite de condition (ou un switch()), j'aurais la logique de tous les différents type de reproduction dans une seule et même méthode, qui contiendra plus de code à chaque nouveau type de reproduction. C'est donc pas très viable.

Je dois avoir un problème dans mon interprétation de l'ECS, peut-être arriverez-vous à m'éclairer.

La solution que j'ai prise pour l'instant est de contenir les systèmes (les comportements) dans l'entité et de ne pas utiliser de composants. Chaque entité contient donc une instance de ComportementReproducteur ou ComportementAlimentaire, par exemple. Ce qui est en fait le pattern Strategy décrit dans le tuto Java, mais j'aimerais réussir à appliquer un ECS complet.

Et une petite question : l'ajout d'une entité en cours de route (dans un bête menu console à chaque tour) doit se faire avant de gérer l'évolution des entités déjà existantes, ou l'inverse ?

Édité par louk

+0 -0

Je pense que trop peu de personne maîtrisent vraiment le sujet pour pouvoir vraiment te répondre. Cependant je peux essayer de te répondre un peu :)

1er et 2e hic : Il s'agit là d'un problème de conception indépendant de la modélisation ECS. Pour moi il faudrait que tu utilises le polymorphisme (que ce soit du pointeur de fonction, de l'héritage on s'en fiche). En fait, même si tu suis un ECS, les systèmes sont (pour moi) ni plus ni moins que des programmes objets standards. Si tu as un système de reproduction, il s'agit (encore pour moi hein, pense pas que c'est absolument comme ça) en réalité d'une façade) simplifié qui va faire les traitements de création de poisson (ici c'est une factory ou ses variantes), de choix d'algorithme (le pattern strategy) et un peu de polymorphisme, ce qui va te permettre de gérer de manière propre toute tes façons de reproduire, tout en créant les poissons qu'il faut. Comme une classe ne doit avoir qu'une fonction, les systèmes sont (toujours imho) des classes qui cachent tout un tas de classe qui elles n'ont qu'un seul rôle.

Édité par Ricocotam

+1 -0

Bon, je m'ennuyais un peu et je me prenais la tête avec l'orienté-objet en Fortran, quand je suis retombé sur cet exercice. Voici donc un début d'implémentation du Fortraquarium !

J'utilise le standard F2003, qui a justement ajouté plein de choses cool pour faire de l'OO. Ce code compile avec gfortran 4.9, et sans doutes d'autres compilateurs, que je n'ai pas sous la main. La partie 1.1 (remplissage et affichage) m'a pris 20min.

Le seul problème est que les tableaux Fortran ont une taille fixe, sauf à jouer avec des allocate/deallocate. Donc pour l'instant j'ai une constante en dur qui fixe le maximum de poisson/algue dans un aquarium. On verra si j'ai la foi de refaire un bout de liste chainée un jour.

EDIT: j'ai rien dit, on peut utiliser move_alloc pour faire croitre un tableau.

Édité par Luthaf

Mon Github — Tuto Homebrew — Article Julia

+0 -0

Bonjour, j'ai fais un projet dans le cadre de mes cours et je me suis très fortement inspiré de cette exercice. Je ferais d'ailleurs un poste d'ici peu :)

Je dois faire un dossier et je veux parler de cet exercice, ce qui parait évident. J'aimerais donc avoir quelques infos dessus que je n'ai pas trouvé.. Premièrement l'auteur s'il y en a un ^^. Et à celui (ou ceux) qui ont créé l'exercice, quel était le but ? Dernière petite question aux membres cette fois, qu'avez vous appris en le faisant, si vous avez appris quelque chose ?

Merci beaucoup ! :)

+0 -0

J'ai commencé il n'y a pas longtemps la POO, du coup c'est un super entrainement, MERCI

Labtec__007

J'ai justement eu à monter un cours OO au boulot, et merci !!
Je ne vous dis pas à quel point il est difficile de trouver des exemples/exercices OO qui ne soient pas des exos de modélisations de BdDs (genre, de quoi sont foutus livres, auteurs, client, bibliothèques).

+1 -0

Personne ne l'a vraiment terminé avec l'ECS. Je me suis au départ lancé là-dedans mais depuis j'ai arrêté et suis passé à autre chose. Je pense pas prendre le temps pour l'instant mais peut-être que j'y retournerai un jour.

+0 -0

L'ECS permet de repousser les limites de l'objet (et c'est aussi valable pour le fonctionnel) quand on a besoin d'avoir des comportements extrêmement variés, composables et changeant sur des objets "équivalents". Le truc c'est que tout ce qui était implicite (typiquement la résolution de type pour l'appel de fonctions) ne l'est plus et donc c'est à toi d'implémenter tout la mécanique derrière.

Ce qui prend finalement du temps, ce n'est pas modéliser le problème pour l'ECS (ça, c'est quasiment simple), non le problème c'est d'implémenter un ECS générique et flexible.

Édité par Ksass`Peuk

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein [Tutoriel Frama-C WP]

+0 -0

Salut.

J'ai récemment découvert un langage, Pony, et j'ai réalisé le Javaquarium pour prendre en main la partie objet. A priori, peu d'entre vous connaissent ce langage, une petite présentation s'impose donc.

Pony est un langage orienté acteur (à la manière d'Erlang), avec une killer feature nommée « reference capabilities ». Pour résumer, c'est un attribut qui définit ce qu'il est possible de faire avec la référence, et quels sont les alias de la référence qui peuvent exister. Quelques exemples :

  • R est une référence box dans un acteur A
    • R peut être utilisée uniquement en lecture
    • Des alias de R en lecture et en écriture peuvent exister, mais uniquement dans A
  • R est une référence iso dans un acteur A
    • R peut être utilisée en lecture et en écriture
    • Aucun alias de R ne peut exister
  • R est une référence val dans un acteur A
    • R peut être utilisée uniquement en lecture
    • Des alias de R en lecture peuvent exister dans d'autres acteurs

Tout cela permet de garantir l'absence de data races à la compilation (une preuve mathématique est en cours de réalisation). Ce système de capabilities est obscur au début, je vous conseille de regarder les conférences faites par l'auteur du langage (liens sur le site), il explique vachement bien.
Pony offre aussi des garanties sur la validité des objets (pas de références nulles), les exceptions, etc.

Avec tout ça, on pourrait penser que le langage est hyper restrictif et inutilisable en pratique, mais il n'en est rien. On a accès à des éléments extrêmement flexibles et expressifs. Par exemple, l'héritage n'existe pas en Pony. A la place, un système de composition de types associé à du pattern matching permet de faire du polymorphisme.

Bref, vous l'aurez probablement compris, mon impression actuelle du langage ressemble à peu près à ceci :

Shut up and take my money

Voici donc le Javaquarium en Pony, accompagné de commentaires à propos des particularités du langage. Je n'ai pas utilisé d'acteurs, je ne parle donc pas de ceux-ci dans les commentaires.

  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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
use "collections"
use "promises"
use "random"
use "term"
use "time"

// Une primitive est similaire à une classe, mais ne peut pas contenir
// de variables membres.
// De plus, toutes les instances d'une primitive ont la même identité
// (toutes les références référencent le même objet)

// Les variables et fonctions globales sont interdites en Pony.
// On utilise des primitives pour représenter des constantes
primitive HPAtBirth fun apply(): U64 => 10
primitive MaxAge fun apply(): U64 => 20
primitive MassPerSeaweed fun apply(): U64 => 10

primitive SeaweedGrowth fun apply(): U64 => 1

primitive HungerDamage fun apply(): U64 => 1
primitive HungerThreshold fun apply(): U64 => 5

primitive HerbHeal fun apply(): U64 => 3
primitive CarnHeal fun apply(): U64 => 5

primitive HerbDamage fun apply(): U64 => 2
primitive CarnDamage fun apply(): U64 => 4

primitive MaxSeaweedMass fun apply(): U64 => 50000
primitive MaxFish fun apply(): U64 => 100

// Définition des espèces de poissons

primitive Flounder
primitive Bass
primitive Carp

// Union de types. Un herbivore est une sole ou un bar ou une carpe
type Herbivorous is (Flounder | Bass | Carp)

primitive Grouper
primitive Tuna
primitive ClownFish

type Carnivorous is (Grouper | Tuna | ClownFish)

type Species is (Herbivorous | Carnivorous)

type Mono is (Carp | Tuna)
type Sequential is (Bass | Grouper)
type Opportunistic is (Flounder | ClownFish)

// Une classe
class Fish
  // Variables membres, ou champs.
  // Un champ est déclaré sous la forme
  // var/let nom: Type (= initialiseur)
  // var désigne une référence réassignable, let une référence fixe
  var _name: String
  var _is_male: Bool
  let _specie: Species
  // Sucre syntaxique : Type() <=> Type.create().apply()
  var _health: U64 = HPAtBirth()
  var _age: U64 = 0
  let _aquarium: Aquarium
  
  // Constructeur
  // On doit obligatoirement initialiser tous les champs qui n'ont pas d'initialiseur par défaut
  new create(nm: String, sp: Species, aq: Aquarium, ml: Bool) =>
    _name = nm
    _specie = sp
    _aquarium = aq
    // Pattern matching. On peut matcher sur des valeurs, des types ou les deux.
    match _specie
    | let l: Sequential => _is_male = true
    else
      _is_male = ml
    end
    
  // ref est une des 6 "reference capabilities"
  // Pour simplifier, une méthode déclarée ref ne peut être appelée que sur des objets mutables
  fun ref update() =>
    _health = _health - _health.min(1)
    _age = _age + 1
    // x == y <=> identité structurale (les champs des objets ont la même valeur)
    if _age == (MaxAge() / 2) then
      match _specie
      | let l: Sequential => _is_male = false
      end
    end
    if dead() then
      return
    end
    if _health <= HungerThreshold() then
      let hp =
        match _specie
        // En Pony, tout est expression, et toute expression retourne une valeur
        // Un if retourne la dernière expression dans son corps
        | let l: Herbivorous => if _aquarium.eat_seaweeds() then HerbHeal() else 0 end
        | let l: Carnivorous => if _aquarium.eat_fish(_specie) then CarnHeal() else 0 end
        else
          0
        end
      _health = _health + hp
    else
      // Bloc pouvant lever une exception
      // Les exceptions de Pony n'ont pas de type dynamique.
      // Elles doivent obligatoirement être attrapées
      try
        let partner = _aquarium.search_partner()
        // x is y <=> égalité identitaire (les références désignent le même objet)
        if (this is partner) or not (_specie is partner.specie()) then
          return
        end
        match _specie
        | let l: Opportunistic => if male() == partner.male() then _is_male = not _is_male end
        else
          if male() == partner.male() then
            return
          end
        end
          _aquarium.birth(_specie)
      end
    end
  
  fun ref damage() =>
    _health = if _health <= CarnDamage() then 0 else _health - CarnDamage() end

  // Une fonction qui retourne une valeur retourne également la dernière expression dans son corps
  fun name(): String => _name
  fun ref rename(new_name: String) => _name = new_name

  fun male(): Bool => _is_male

  fun specie(): Species => _specie
  
  fun dead(): Bool => (_health == 0) or (_age >= MaxAge())
  
  fun print(out: StdStream) =>
    out.print(_name + ", " + if _is_male then "male " else "female " end +
      match _specie
      | Flounder => "flounder"
      | Bass => "bass"
      | Carp => "carp"
      | Grouper => "grouper"
      | Tuna => "tuna"
      | ClownFish => "clown fish"
      else
        "unknown"
      end +
      ", " + _health.string() + " hp, age " + _age.string()
    )

class Aquarium
  var _seaweed_mass: U64 = 0
  let _fishs: Array[Fish] = Array[Fish]
  let _births: Array[Fish] = Array[Fish]
  let _deads: Array[String] = Array[String]
  var _turn: U64 = 0
  var _i: U64 = 0
  var _should_advance: Bool = true
  let _rand: Dice
  var _anon_count: U64 = 0
  
  new create() =>
    let tm = Time.now()
    let seed: U64 = (tm._1 xor tm._2).u64()
    _rand = Dice(MT(seed))
  
  fun ref add_seaweeds(count: U64) =>
    _seaweed_mass = (_seaweed_mass + (MassPerSeaweed() * count)).min(MaxSeaweedMass())
  
  fun ref remove_seaweeds(count: U64) =>
    _seaweed_mass = if _seaweed_mass <= (MassPerSeaweed() * count) then
      0
    else
      _seaweed_mass - (MassPerSeaweed() * count)
    end
 
  fun ref add_fish(name: String, sp: Species, male: Bool = false) =>
    _fishs.push(Fish(name, sp, this, male))
  
  // Le "?" indique une fonction partielle. Une exception peut sortir de la fonction
  fun ref remove_fish(idx: U64) ? =>
    _fishs.delete(idx)
  
  fun ref rename_fish(idx : U64, new_name: String) ? =>
    // Sucre syntaxique : objet(foo) <=> objet.apply(foo)
    _fishs(idx).rename(new_name)
  
  fun ref birth(sp: Species) =>
    if (_fishs.size() + _births.size()) < MaxFish() then
      _anon_count = _anon_count + 1
      _births.push(Fish("Anonymous" + _anon_count.string(), sp, this, (_rand(1, 2) - 1) == 0))
    end
  
  fun ref update() =>
    _deads.clear()
    _births.clear()
    _seaweed_mass = (_seaweed_mass + (SeaweedGrowth() * (_seaweed_mass / MassPerSeaweed()))).min(MaxSeaweedMass())
    _i = 0
    while _i < _fishs.size() do
      try
        _fishs(_i).update()
        if _fishs(_i).dead() then
          _update_on_death(_i)
        end
      end
      // Un assignement retourne l'ancienne valeur de la variable
      if _should_advance = true then
        _i = _i + 1
      end
    end
    for bir in _births.values() do
      _fishs.push(bir)
    end
    _turn = _turn + 1
  
  fun ref eat_seaweeds(): Bool =>
    if _seaweed_mass >= HerbDamage() then
      _seaweed_mass = _seaweed_mass - HerbDamage()
      true
    else
      false
    end
  
  fun ref eat_fish(specie: Species): Bool =>
    let i = _rand(1, _fishs.size()) - 1
    try
      if _fishs(i).specie() is specie then
        false
      else
        _fishs(i).damage()
        if _fishs(i).dead() then
          _update_on_death(i)
        end
        true
      end
    else
      false
    end
  
  fun ref search_partner(): Fish ? =>
    let i = _rand(1, _fishs.size()) - 1
    _fishs(i)
  
  fun ref _update_on_death(i: U64) =>
    try
      _deads.push(_fishs(i).name())
      _fishs.delete(i)
    end
    _should_advance = i > _i
  
  fun print(out: StdStream) =>
    out.print("Turn " + _turn.string())
    out.print("Seaweeds : " + (_seaweed_mass / MassPerSeaweed()).string() + " (Total mass : " + _seaweed_mass.string() + ")")
    out.print("Fishs :")
    var i = U32(0)
    for fish in _fishs.values() do
      out.write("#" + i.string() + " ")
      fish.print(out)
      i = i + 1
    end
    if _births.size() > 0 then
      out.print("")
      for bir in _births.values() do
        out.print(bir.name() + " is born!")
      end
    end
    if _deads.size() > 0 then
      out.print("")
      for dead in _deads.values() do
        out.print(dead + " is dead!")
      end
    end
    out.print("")

// Les entrées/sorties sont asynchrones en Pony.
// Cette classe gère les entrées console
class CommandHandler is ReadlineNotify
  let _commands: Array[String] = Array[String]
  let _aq: Aquarium
  let _out: StdStream
  
  new create(aq: Aquarium, out: StdStream) =>
    _commands.push("quit")
    _commands.push("print")
    _commands.push("update")
    _commands.push("add")
    _commands.push("kill")
    _commands.push("rename")
    _aq = aq
    _out = out
    
  fun ref apply(line: String, prompt: Promise[String]) =>
    // recover permet de changer la capability d'une expression.
    // Il y a bien sûr des restrictions sur les possibilités.
    // Ici, on transforme un iso (référence unique) en ref (référence pouvant avoir des alias)
    let args = recover ref line.split() end
    var i = U64(0)
    while i < args.size() do
      try
        if args(i) == "" then
          args.delete(i)
        else
          i = i + 1
        end
      end
    end
    try
      match args(0)
      | "quit" => prompt.reject()
      | "print" => _aq.print(_out)
      | "update" => _aq.update(); _aq.print(_out)
      | "add" => _handle_add(args)
      | "kill" => _aq.remove_fish(args(1).u64())
      | "rename" => _aq.rename_fish(args(1).u64(), args(2))
      else
        error
      end
    else
      _out.print("Invalid command")
    end
    prompt("> ")
  
  fun ref tab(line: String): Seq[String] box =>
    let r = Array[String]
    for command in _commands.values() do
      if command.at(line, 0) then
        r.push(command)
      end
    end
    r
  
  fun ref _handle_add(args: Array[String]) ? =>
    match args(1)
    | "fish" =>
      let name = args(2)
      let specie = match args(3)
      | "flounder" => Flounder
      | "bass" => Bass
      | "carp" => Carp
      | "grouper" => Grouper
      | "tuna" => Tuna
      | "clownfish" => ClownFish
      else
        error
      end
      let sex = match args(4)
      | "male" => true
      | "female" => false
      else
        error
      end
      _aq.add_fish(name, specie, sex)
    | "seaweeds" =>
      let count = args(2).i64()
      if count >= 0 then _aq.add_seaweeds(count.abs()) else _aq.remove_seaweeds(count.abs()) end
    else
      error
    end

// Le programme commence dans le constructeur de l'acteur Main
actor Main
  // env contient des éléments comme les arguments du programme, les flux d'entrée/sortie standard, etc
  new create(env: Env) =>
    env.out.print("Use 'quit' to quit")
    // On va transmettre ces objets à un autre acteur, on doit donc créer des références uniques.
    let aq = recover Aquarium end
    let term = recover ANSITerm(Readline(recover CommandHandler(consume aq, env.out) end, env.out), env.input) end
    term.prompt("> ")
    
    let notif = recover
      // Déclaration d'objet anonyme
      object is StdinNotify
        let term: ANSITerm = consume term
        
        fun ref apply(data: Array[U8] iso) =>
          term(consume data)
        
        fun ref dispose() =>
          term.dispose()
      end
    end
    
    env.input(consume notif)

Édité par Praetonus

Mon Github | Pony : Un langage à acteurs sûr et performant

+2 -0

Tout cela permet de garantir l'absence de data races à la compilation (le système a été prouvé mathématiquement).

Praetonus

Ben, le problème c'est que dans tous leurs papiers la preuve est mentionnée dans les "future works" et que si preuve il y a, c'est difficile de la trouver (et pas de précision non plus sur le moyen de preuve utilisé).

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein [Tutoriel Frama-C WP]

+0 -0

Effectivement, j'ai relu les papiers, la question des data races n'est pas prouvée. J'ai été un peu trop enthousiaste. :)
Néanmoins, le modèle est cohérent en pratique (et délicieux à utiliser). Certes, la différence entre théorie et pratique est variable (surtout en preuve), donc je n'irai pas jusqu'à dire « y'a plus qu'à », mais ça me semble en très bonne voie.

Mon Github | Pony : Un langage à acteurs sûr et performant

+0 -0

Ouais mais du coup ça me paraissait bizarre :lol: . J'ai pu rencontrer des doctorants/profs qui bossent sur différents projets plus ou moins liés (à une école d'été cette année). Entre autres le langage Encore et le GC Orca qui doit venir s'intégrer à Pony. Et ils n'avaient pas parlé de preuve complète et encore moins de preuve outillées (Coq, Isabelle, etc …).

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein [Tutoriel Frama-C WP]

+0 -0

Petit HS : quand j'ai vu "if (this is partner …)", j'ai pas pu m'empêcher de remplacer dans ma tête "partner" par "sparta".

Ce langage est super intéressant, ça fait un peu un mélange de Python, de Caml et de Go. J'y penserai pour un projet un jour !

+2 -0

En fait le plus casse-pied c'est pas tant l'héritage, le stratégies (régime alimentaire, reproduction) ou quoi, c'est d'arriver à trouver un moyen élégant d'éviter les concurrent modification exceptions ^^

On a envie de juste parcourir une liste d'être-vivants et de leur dire "vivez votre vie". Dans le même temps, on aimerait bien que quand ils font qqch en rapport avec l'aquarium, ça mette à jour l'aquarium en conséquence (typiquement qu'il se nettoie des cadavres tout seul, que les petits enfants s'ajoutent instantanément).

Ça oblige à "cloner" les listes de poissons/plantes au début pour travailler sur la liste telle qu'elle était lorsque le tour a démarré.

J'trouve ça un peu moche

EDIT : mais en même temps ça a du sens… On fait évoluer les poissons qu'on avait au départ. Mais bon c'est juste en termes de code que fishes.each {} c'est plus joli que fishes.findAll{}.each{}

EDIT² : Autre truc extrêmement casse-pied, c'est l'aspect aléatoire qui ne permet pas d'écrire de petits tests unitaires. Du coup on est obligé de mettre la partie tirage aléatoire à part pour pouvoir écrire des tests déterministes (style : un mérou ne peut pas manger une sole).

Édité par Javier

Happiness is a warm puppy

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte