import 'dart:math'; import 'package:flutter/material.dart'; /// Taquin classique (15-puzzle) : une case vide, seule une tuile adjacente à /// la case vide peut glisser dedans. Équivalent de `SlidingPuzzle.tsx` (web). class GuidedPathSlidingPuzzle extends StatefulWidget { final String imageUrl; final int rows; final int cols; final Color accentColor; final bool isGame; final VoidCallback onSolved; const GuidedPathSlidingPuzzle({ Key? key, required this.imageUrl, required this.rows, required this.cols, required this.accentColor, required this.isGame, required this.onSolved, }) : super(key: key); @override State createState() => _GuidedPathSlidingPuzzleState(); } class _GuidedPathSlidingPuzzleState extends State { // tiles[slot] = pièce d'origine occupant ce slot ; -1 = case vide. late List _tiles; bool _won = false; bool _showHint = false; int get _total => widget.rows * widget.cols; @override void initState() { super.initState(); _shuffle(); } @override void didUpdateWidget(GuidedPathSlidingPuzzle old) { super.didUpdateWidget(old); if (old.rows != widget.rows || old.cols != widget.cols) _shuffle(); } List _neighboursOf(int idx) { final row = idx ~/ widget.cols; final col = idx % widget.cols; final list = []; if (row > 0) list.add(idx - widget.cols); if (row < widget.rows - 1) list.add(idx + widget.cols); if (col > 0) list.add(idx - 1); if (col < widget.cols - 1) list.add(idx + 1); return list; } void _shuffle() { final arr = List.generate(_total - 1, (i) => i)..add(-1); var emptyIdx = _total - 1; final rand = Random(); for (int n = 0; n < 200; n++) { final neighbours = _neighboursOf(emptyIdx); final pick = neighbours[rand.nextInt(neighbours.length)]; final tmp = arr[emptyIdx]; arr[emptyIdx] = arr[pick]; arr[pick] = tmp; emptyIdx = pick; } _tiles = arr; _won = false; } void _onTapSlot(int slotIdx) { if (_won) return; final emptyIdx = _tiles.indexOf(-1); if (!_neighboursOf(emptyIdx).contains(slotIdx)) return; setState(() { final tmp = _tiles[emptyIdx]; _tiles[emptyIdx] = _tiles[slotIdx]; _tiles[slotIdx] = tmp; final win = _tiles.asMap().entries.every( (e) => e.key == _tiles.length - 1 ? e.value == -1 : e.value == e.key); if (win) { _won = true; Future.delayed(const Duration(milliseconds: 400), () { if (mounted) widget.onSolved(); }); } }); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final board = constraints.maxWidth.isFinite ? constraints.maxWidth : MediaQuery.of(context).size.width - 64; final tileW = board / widget.cols; final tileH = board / widget.rows; return SizedBox( width: board, height: board, child: Stack( children: [ if (_showHint && !_won) Positioned.fill( child: Opacity( opacity: 0.25, child: Image.network(widget.imageUrl, fit: BoxFit.cover), ), ), for (int slot = 0; slot < _total; slot++) if (_tiles[slot] != -1) AnimatedPositioned( key: ValueKey('slide-${_tiles[slot]}'), duration: const Duration(milliseconds: 220), curve: Curves.easeOut, left: (slot % widget.cols) * tileW, top: (slot ~/ widget.cols) * tileH, width: tileW, height: tileH, child: _SlidingTile( piece: _tiles[slot], rows: widget.rows, cols: widget.cols, boardSize: board, imageUrl: widget.imageUrl, onTap: () => _onTapSlot(slot), ), ), if (_won) Positioned.fill( child: Container( decoration: BoxDecoration( color: Colors.black.withOpacity(0.35), borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: const Text( 'Puzzle résolu !', style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600), ), ), ), if (!_won) Positioned( right: 8, bottom: 8, child: GestureDetector( onTap: () => setState(() => _showHint = !_showHint), child: Container( width: 36, height: 36, decoration: BoxDecoration( shape: BoxShape.circle, color: _showHint ? widget.accentColor : (widget.isGame ? Colors.white.withOpacity(0.1) : Colors.white), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 8)], ), child: Icon( Icons.remove_red_eye, size: 18, color: _showHint ? (widget.isGame ? const Color(0xFF1a1305) : Colors.white) : (widget.isGame ? const Color(0xFFD4AF37) : const Color(0xFF54636E)), ), ), ), ), ], ), ); }, ); } } class _SlidingTile extends StatelessWidget { final int piece; final int rows; final int cols; final double boardSize; final String imageUrl; final VoidCallback onTap; const _SlidingTile({ required this.piece, required this.rows, required this.cols, required this.boardSize, required this.imageUrl, required this.onTap, }); @override Widget build(BuildContext context) { final row = piece ~/ cols; final col = piece % cols; return GestureDetector( onTap: onTap, child: Container( margin: const EdgeInsets.all(1.5), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.25), blurRadius: 6)], ), child: ClipRRect( borderRadius: BorderRadius.circular(6), child: OverflowBox( maxWidth: boardSize, maxHeight: boardSize, alignment: Alignment( cols == 1 ? 0.0 : -1.0 + (col * 2 / (cols - 1)), rows == 1 ? 0.0 : -1.0 + (row * 2 / (rows - 1)), ), child: SizedBox( width: boardSize, height: boardSize, child: Image.network(imageUrl, fit: BoxFit.cover), ), ), ), ), ); } }