Some random comments that spontaneously come to mind.
First of all, in play(), the definitions of player1 and player2 are quite repetitive. A way to simplify this would be to create a single vector of 2*turns dice rolls, then iterate over pairs of dice rolls using the chunks() slice method.
If you allow yourself to use itertools, you can also simplify things further and reduce memory usage to O(1) by rewriting the beginning of play() into…
let player_rolls = itertools::repeat_call(generator)
The fact that play() mixes together I/O and “application logic” may not be of everyone’s taste either. A cleaner approach would be for play() to return an enum modeling all possible game outcomes (player N wins + draw), and extracting all the result reporting I/O logic into main().
The error handling in the main loop is somewhat inelegant. I think nicer error handling code could be produced by handling unexpected termination (e.g. stdout flush failure) via expect(), which is basically a self-documenting variant of unwrap(), and centralizing the “expected” termination scenarios into a single match statement using either Result methods (map, map_err, etc) or a dedicated loop iteration functions that uses early exit.
If you were going through the futile exercise of optimizing this toy code for performance, you could as well reduce dynamic memory allocations by moving the definition of “buffer” outside of the main loop and clearing it on every loop iteration instead.
EDIT: As a matter of taste, I would also rename “between” into “dice”, since
dice.sample(rng) has imo a slightly clearer intent than